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:
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.
console/| File | Purpose |
|---|---|
versions.tf | Provider pins (terraform ≥ 1.9, aws ≥ 5.0) |
variables.tf | 11 inputs (project name, account, region, perm boundary, 2 KMS keys, DDB cache, 3 Cognito values, optional custom domain) |
auth.tf | One 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.tf | S3 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.tf | Lambda (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.tf | console_url, cloudfront_distribution_id, cognito_client_id, sdk_base_url, frontend_bucket_name, lambda_function_name |
cloudfront/spa-router.js | SPA viewer-request function. Static-asset allowlist + /sdk/* passthrough + fallback rewrite to /index.html for client-side routes (/callback after OAuth) |
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:
iframe — load cartridge URL in the main pane's iframe; postMessage the JWT once the cartridge signals { type: 'cartridge-ready' }.external — open the cartridge's standalone URL in a new tab (Obs today).comingsoon — disabled tile, no-op click.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).
console/sdk/v1/css/tokens.css — PayCargo design tokens (single source of truth). Cartridges <link> this to inherit the visual identity.js/cartridge-auth.js — PCSDK.auth.ready() returns a Promise resolving with the access token. Inside an iframe, posts cartridge-ready to parent and awaits the cartridge-auth response. Outside an iframe (standalone), rejects so the cartridge can fall back to its own login modal.
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).
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).
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.
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.
| Service | Resource | Cost / month | Notes |
|---|---|---|---|
| S3 | Console bucket (frontend + SDK) | < $0.01 | ~62KB total, KMS-encrypted with the shared CMK (no extra key cost) |
| CloudFront | Distribution + data transfer | < $0.05 | PriceClass_100 (US/CA/EU). $0.085/GB · we're measured in KB |
| CloudFront Function | SPA router invocations | $0.00 | First 2M invocations/month free |
| Lambda | Console backend | $0.00 | Within the 1M invocations + 400K GB-sec free tier indefinitely |
| Lambda Function URL | — | $0.00 | No additional charge; OAC invocation auth is free |
| CloudWatch Logs | Backend log group (90d retention) | < $0.01 | $0.50/GB ingest · <1MB/month |
| Cognito | App client on existing pool | $0.00 | App clients are free; MAU billing shared with obs-app's pool |
| KMS | Per-request decrypt charges | < $0.01 | Re-uses ceo-office-low-risk + eng-low-risk CMKs already paid for |
| IAM, OACs, certs | — | $0.00 | No 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.
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.
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
}
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>
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.
/sdk/v1/auth/. Add component CSS (panels, buttons, tables, badges) into /sdk/v1/components/. Future cartridges inherit visual + auth + components from a single import.CARTRIDGES from hardcoded Lambda array to DynamoDB. Add Admin tab in Console for register/edit/disable. Bolt on data governance (which CMK tier this cartridge writes to), retention policy (how long it keeps state), AI budget (model spend ceiling), SoR access (which systems of record it can call). Cartridge cannot deploy without declaring these.loadMode: 'iframe' is a frontend-only change (rip out Obs's login modal, depend on the SDK auth helper instead).<template> in the DOM; JS materializes it after auth. DOM tampering on the overlay reveals nothing.