PayCargo · AI-Sandbox · Step 6.g

6.gConsole v0 — build walkthrough

The MAME-style platform shell. What got built, what it cost, how to add a cartridge.
Status: Live in AI-Sandbox · URL: https://da7cclwu7mps0.cloudfront.net/
Deployed
Console v0 is running in the AI-Sandbox account. Same Cognito pool as Obs (sign in once, JWT carries). Separate S3 + CloudFront + Lambda. SDK v1 shipping at /sdk/v1/*. No new pool, no new MFA enrollment, no Obs refactor — Obs continues running standalone at its current URL.

Why a separate Console

The Console is the substrate's front door. Cartridges (vibe apps) plug into it; the Console doesn't deploy them — it lists them, gates them by permission, and launches the selected one inside an iframe. This is the NES / MAME emulator model: one machine, many ROMs, permission-gated.

Keeping Console separate from Observability is the architecture-validating choice:

Architecture

Console (this build)                     https://da7cclwu7mps0.cloudfront.net/
  ├─ Launcher       cartridge tiles, permission-gated, MAME-style
  ├─ Iframe loader  injects JWT to cartridge via postMessage
  ├─ SDK            /sdk/v1/css/tokens.css · /sdk/v1/js/cartridge-auth.js
  └─ Auth           Cognito user pool client on obs-app's existing pool

Cartridges (registered with Console)
  ├─ Observability  external — opens at its own URL in new tab (today)
  ├─ Chatbot        coming soon — first native cartridge (tomorrow)
  └─ Sales          coming soon — Phase C

One Cognito pool. Console registers as a NEW app client on the existing obs-app pool so users created via the Admin tab in Obs can immediately sign in to the Console with the same credentials and TOTP. JWT issued for the Console client is valid against any cartridge that respects the same pool issuer.

What got built

Terraform module · console/

FilePurpose
versions.tfProvider pins (terraform ≥ 1.9, aws ≥ 5.0)
variables.tf11 inputs (project name, account, region, perm boundary, 2 KMS keys, DDB cache, 3 Cognito values, optional custom domain)
auth.tfOne aws_cognito_user_pool_client registered on the existing obs-app pool. USER_SRP_AUTH + REFRESH_TOKEN_AUTH + PKCE code grant. No new pool.
frontend.tfS3 bucket + KMS encryption + public-access block + 2 OACs (S3 + Lambda) + SPA router CloudFront Function + 6 frontend asset uploads + 2 SDK v1 asset uploads + runtime config.json + CloudFront distribution with /api/* ordered behavior + bucket policy
backend.tfLambda (Node 20, 512MB) + Function URL (AWS_IAM auth) + IAM role with perm boundary + scoped policy (logs, Cognito client discovery, DDB cache, KMS Decrypt) + CW log group encrypted by backend KMS + cloudfront permissions (InvokeFunction + InvokeFunctionUrl, both required per the OAC doctrine in Step 4.5.b)
outputs.tfconsole_url, cloudfront_distribution_id, cognito_client_id, sdk_base_url, frontend_bucket_name, lambda_function_name
cloudfront/spa-router.jsSPA viewer-request function. Static-asset allowlist + /sdk/* passthrough + fallback rewrite to /index.html for client-side routes (/callback after OAuth)

Frontend · console/frontend/

Same 4-step Cognito SRP + TOTP login modal as Obs (credentials → TOTP → newpw → mfasetup). Same advanced-security data collector (~300ms auth vs ~20s without). Same bypass-proof <template> shell gating. After auth: MAME-style sidebar with cartridge tiles, topbar with user + sign-out, main pane that hosts either the welcome state or the cartridge iframe.

The cartridge launcher fetches /api/cartridges (auth-gated, server-side filtered by cognito:groups), renders one tile per visible cartridge, and dispatches the click based on loadMode:

Backend Lambda · console/lambda/handler.js

Two endpoints. GET /api/config returns the public bootstrap (region, account, Cognito IDs). GET /api/cartridges verifies the JWT, reads the hardcoded CARTRIDGES array, filters by the caller's cognito:groups, returns the visible list. The Cognito client ID is discovered lazily at cold-start via ListUserPoolClients (same circular-dependency-avoidance pattern as Obs).

SDK v1 first drop · console/sdk/v1/

The SDK is the contract between Console and cartridges. Versioned by URL path: /sdk/v1/ stays mutable for non-breaking improvements; /sdk/v2/ ships when a breaking change lands and old cartridges keep working on v1 until they opt in (npm-semver-analog per Step 6.f).

Composition wiring

substrate/environments/ai-sandbox/main.tf gained a module "console_app" block after observability_app, depending on it (so Cognito outputs exist at Console plan time). New outputs on obs-app: cognito_user_pool_arn + cognito_user_pool_domain (existing cognito_user_pool_id output reused).

CI workflow

Both terraform-plan.yml and terraform-apply.yml gained a Install Lambda dependencies (console) step before terraform init, parallel to the existing obs-app step.

What it costs

Console adds the following per-month AWS spend on top of the existing substrate. All figures assume the current low-traffic showcase usage (10s of sign-ins/month, <1MB of logs); scale them linearly with traffic.

ServiceResourceCost / monthNotes
S3Console bucket (frontend + SDK)< $0.01~62KB total, KMS-encrypted with the shared CMK (no extra key cost)
CloudFrontDistribution + data transfer< $0.05PriceClass_100 (US/CA/EU). $0.085/GB · we're measured in KB
CloudFront FunctionSPA router invocations$0.00First 2M invocations/month free
LambdaConsole backend$0.00Within the 1M invocations + 400K GB-sec free tier indefinitely
Lambda Function URL$0.00No additional charge; OAC invocation auth is free
CloudWatch LogsBackend log group (90d retention)< $0.01$0.50/GB ingest · <1MB/month
CognitoApp client on existing pool$0.00App clients are free; MAU billing shared with obs-app's pool
KMSPer-request decrypt charges< $0.01Re-uses ceo-office-low-risk + eng-low-risk CMKs already paid for
IAM, OACs, certs$0.00No charge for these resource types

Total additional monthly cost: ~$0.05 (essentially free). The expensive substrate components — Cognito pool, KMS keys, DynamoDB cache table, the VPC — are all shared with obs-app and already paid for. Console only adds a tiny CloudFront distribution and a tiny Lambda, both well below their respective free tiers at current usage.

At 1,000 monthly active users (vs the current ~5), expect this to grow to roughly $10-15/month: CloudFront data transfer climbs with each cartridge launch, Lambda invocations grow with each /api/cartridges request, CloudWatch ingest grows linearly. Still trivial relative to the value of platform-pattern isolation.

The platform pricing payoff

Because Console reuses the substrate components — pool, KMS, cache, network — each new cartridge added going forward incurs the same ~$0.05/month marginal cost. The first cartridge proved the substrate; every subsequent cartridge proves the amortization. Ten cartridges = ~$0.50/month additional.

How to add a new cartridge

  1. Deploy the cartridge as its own vibe app (S3 + CloudFront + Lambda + Cognito client on the shared pool, like Obs).
  2. Edit console/lambda/handler.js, add an entry to the CARTRIDGES array:
    {
      id: 'my-cartridge',
      name: 'My Cartridge',
      description: 'One-line pitch',
      icon: 'fa-rocket',           // Font Awesome solid icon
      color: 'purple',             // tile accent (blue/purple/green/orange)
      url: 'https://my-cartridge.cloudfront.net/',
      loadMode: 'iframe',          // 'iframe' | 'external' | 'comingsoon'
      requiredGroups: ['my-group'] // empty array = visible to all signed-in users
    }
  3. If the cartridge is native (loadMode: 'iframe'), it imports the SDK:
    <link rel="stylesheet"
          href="https://da7cclwu7mps0.cloudfront.net/sdk/v1/css/tokens.css">
    <script src="https://da7cclwu7mps0.cloudfront.net/sdk/v1/js/cartridge-auth.js">
    </script>
    <script>
      PCSDK.auth.ready().then(({ accessToken, idToken, email }) => {
        // use accessToken in X-Auth-Token for backend calls
        bootApp(accessToken, email);
      });
    </script>
  4. Commit, PR, plan, apply. The new cartridge tile appears for every user whose cognito:groups superset includes requiredGroups.

Future iterations move the cartridge inventory from the Lambda's hardcoded array to a DynamoDB table editable via an Admin tab in the Console, with per-cartridge governance attributes (data tiers, retention policy, AI budget, allowed systems of record). That work is scoped post-60-day per the platform roadmap.

What's next

Doctrine pins reinforced