PAYCARGO · AI-SANDBOX · PROCESS

4.5Observability Vibe App

The first vibe app deployed on the substrate. Static HTML/JS on S3+CloudFront, Lambda backend, dual-auth (Cognito + future SSO), agent-augmented narrative. Built for engineering executives and the platform team to see what the AI platform is doing in real time.
Block: 4.5 of N (sub-step between substrate completion and enforcement flip) · Audience: CTO, VP Eng, CEO, CISO, the operator + operations team + future SecOps · Cost: ~$5-10/month all-in · Stack: 100% serverless (no EC2, no Fargate, no RDS)
Designed

State: Design and cost model documented. Terraform module and Lambda code not yet committed. Implementation queued behind the doc review.

Why "design first": The vibe app is the first thing investors / strategic leadership / a security reviewer will look at. Getting the architecture and cost story straight before code goes in saves a rebuild.

Executive Summary

A polished, exec-facing dashboard that shows what the AI platform is doing, what it costs, and whether it is safe — built using the same substrate it watches.

The observability vibe app is the first concrete consumer of the substrate. It reads from the CloudWatch dashboards + log groups deployed in Step 4, renders them in three audience-specific views (operators, exec, audit), and generates a daily narrative summary via Bedrock. Critically, it is itself a vibe app — built on the same patterns the Sales monthly reports app and every future PayCargo internal app will use. The tech leader value is twofold: (1) you can show this to anyone — executive leadership, strategic leadership, an investor, a CISO — and they will understand what the platform does in 60 seconds; (2) it proves the doctrine, because the same audit/encryption/guardrail rules that protect production data also protect the app that watches the platform.

StatusDesigned
Cost~$5-10/month
EffortPlan: this doc
Build: ~5 days
Review: 30 min
RiskLow

If skipped: Operators and execs continue to read three separate CloudWatch dashboards in the AWS Console. No polished demo for strategic leadership / investors. No proof that vibe apps can be built on the substrate. the external developer next week has no reference implementation.

1. What this vibe app is, in 60 seconds

A web app at https://obs.ai-sandbox.paycargo.com (CloudFront-served). Three audience tabs: Operators, Exec, Audit. Each tab is a single-page view with 4-8 panels. Each panel pulls live data from CloudWatch / CloudTrail / GuardDuty / Bedrock invocation logs via a Lambda backend. A daily 09:00 ET job invokes Bedrock to generate a narrative paragraph ("Yesterday the platform processed 1,247 agent calls across 3 tiers; cost was $4.31; one NFW alert fired against an undocumented destination, investigated and confirmed as test traffic"). The paragraph appears at the top of the Exec tab.

It is itself an agent-assisted application. It reads from the same audit streams every other agent writes to. The same guardrail enforces the narrative generation. The same KMS keys encrypt its data. It is the substrate watching itself.

The one-line pitch for non-technical viewers

It is the dashboard you show an investor to prove the AI platform is operating safely, profitably, and visibly. Every chart corresponds to a real AWS resource that costs real money and produces real audit records. Nothing is mocked.

2. Where it lands — the 9 AWS components

Every box below is a discrete AWS resource Terraform deploys as part of the vibe app module. Each one is priced in § 4.

Frontend · Static

1. S3 bucket (private) for static assets

Hosts index.html, the three tab views, the CSS, and the JS bundle that drives the UI. About 2-5 MB total. Bucket is private — public access disabled. Only CloudFront can read from it via Origin Access Identity (OAI).

Resource

ai-sandbox-obs-app-frontend
Encrypted with audit CMK (consistency with substrate; assets aren't sensitive, but uniform encryption simplifies audit)

Why S3 (not CloudFront Functions / Lambda@Edge / S3 Object Lambda)

Static assets. The cheapest, simplest delivery. No compute on the path. CloudFront handles caching.
Frontend · CDN

2. CloudFront distribution

HTTPS termination, asset caching, custom domain. Origin: the S3 bucket above. ACM certificate for obs.ai-sandbox.paycargo.com. WAF deliberately deferred (Phase 2 if the app ever sees external traffic; today it's internal-only and Cognito gates access).

Cache behavior

/api/* — bypass cache, forward to Lambda Function URL
/* — cache from S3, default TTL 1 day for assets, 60s for HTML

Security

Origin Access Identity locks S3 to CloudFront only
Geo-restriction US+CA (PayCargo's user base)
TLS 1.3 minimum
Auth

3. Cognito User Pool (manual enrollment with TOTP MFA)

The "open today, lock down later" auth path. PayCargo execs and operators enroll with email+password. MFA is required via TOTP authenticator app (Authy, Google Authenticator, 1Password). No SMS MFA — TOTP is more secure and free. Email domain restricted to @paycargo.com.

MFA configuration

software_token_mfa_configuration { enabled = true }
mfa_configuration = "ON" (required for all users)
auto_verified_attributes = ["email"]

Feature flag

var.enable_manual_enrollment = true (today)
Flip to false once SSO federation lands and users have migrated. Manual users stay enrolled; new manual signups blocked.
Auth · Future

4. Cognito SSO federation (Identity Center, Phase 2)

The "real" auth path. Once Identity Center is configured (Phase 2 doctrine), Cognito federates to it via SAML/OIDC. PayCargo SSO becomes the only way in. The Cognito User Pool stays — it just becomes a thin facade over Identity Center for the JS SDK.

Today (Phase 1)

var.enable_sso_federation = false
Module creates the IdP attachment as a no-op when disabled.

Phase 2

Flip enable_sso_federation = true, point at Identity Center, flip enable_manual_enrollment = false. One identity source. Revoke once = revoke everywhere.
API · Compute

5. Lambda backend (Node.js 20)

One function, multiple routes. Invoked via Lambda Function URL (no API Gateway — saves cost, faster cold start). Receives the Cognito JWT, validates it, queries CloudWatch / CloudTrail / GuardDuty / Bedrock invocation log group, returns JSON.

Routes

GET /api/operators/health — real-time platform health
GET /api/exec/summary — exec dashboard data + narrative
GET /api/audit/query — pre-built Logs Insights queries
GET /api/cost/breakdown — Cost Explorer pull

Runtime

Node.js 20, ARM64 (Graviton), 1024 MB
15-second timeout per request
Provisioned concurrency: 0 (cold start acceptable for internal app at this volume)
VPC-attached: no (calls AWS APIs from default Lambda network — read-only)
Data · Existing

6. DynamoDB — agent state table (reused from Step 4)

Cached panel data + the daily narrative. The Lambda backend caches CloudWatch query results for 60 seconds to keep request latency low. The narrative paragraph (generated daily) lives here for fast read.

No new table

ai-sandbox-agent-state already exists (Step 4). The obs app uses a partition session_id = "obs-app" for its cached data. This is the doctrine in action — no new table per app, just well-scoped partitions on existing tables.

Items written

One per panel cache (5-8 items)
One per daily narrative
~10 writes/day, ~thousands of reads/day
AI

7. Bedrock — daily narrative generator

Once per day at 09:00 ET, EventBridge fires a rule that invokes the Lambda backend's generateNarrative handler. The handler queries the last 24h of platform data, formats a prompt, calls Bedrock (Claude Haiku for speed and cost), gets back a 2-paragraph executive summary, and writes it to the DDB cache.

Model + cost

Claude Haiku 4.5 (cheapest, fast)
~3k input tokens, ~600 output tokens per day
Cost: ~$0.002/day = $0.06/month

Guardrail applied

ai-sandbox-default (Step 4 guardrail). Even though the narrative reads platform metadata (no PII), the call passes through the same guardrail every other agent uses. Doctrine: no agent invocation bypasses the guardrail.
Operational

8. EventBridge scheduled rule

Daily cron at 09:00 ET. Triggers the Lambda's narrative-generation handler. Nothing else. ~30 invocations per month.

Identity

9. IAM execution role for the Lambda

Lambda role with the bootstrap permission boundary attached. Permissions: read CloudWatch Logs, read CloudWatch Metrics, read Cost Explorer, read GuardDuty findings, read CloudTrail events, invoke Bedrock with the platform guardrail, read/write the obs-app partition of agent-state DDB, decrypt with audit CMK.

What is deliberately NOT in this list

No Fargate, no ECS, no EKS (no containers — Lambda is sufficient). No RDS, no Aurora (no relational data — DDB existing table handles cache). No API Gateway (Lambda Function URL with Cognito JWT auth is cheaper and sufficient). No ElastiCache (60-second DDB cache is fast enough). No WAF (Cognito gates all access; no public endpoints). No CloudFront Functions / Lambda@Edge (no per-request edge compute needed). No Step Functions (single-function workflows). No separate observability stack (the app uses the substrate's CloudWatch + CloudTrail like everything else).

3. What happens when a user opens the app — call flow

End-to-end, from "the user clicks the bookmark" to "the user sees the Operators dashboard." Every arrow is a real AWS API call that produces a CloudTrail event.

┌──────────────────────────────────────────────────────────────────────────────┐ │ the user's browser: https://obs.ai-sandbox.paycargo.com │ └────────────────────────────┬─────────────────────────────────────────────────┘ │ (1) HTTPS GET / ▼ ┌──────────────────────────────────────────────────────────────────────────────┐ │ CloudFront │ │ • TLS 1.3 termination │ │ • Geo-check: US/CA only │ │ • Cache check: hit on /index.html (60s TTL) │ └────────────────────────────┬─────────────────────────────────────────────────┘ │ (2) Cache miss → S3 GetObject ▼ ┌──────────────────────────────────────────────────────────────────────────────┐ │ S3 bucket ai-sandbox-obs-app-frontend (Origin Access Identity) │ │ • Returns index.html (2 KB) │ └────────────────────────────┬─────────────────────────────────────────────────┘ │ (3) HTML returns to browser │ JS bundle loads │ Cognito client JS initializes ▼ ┌──────────────────────────────────────────────────────────────────────────────┐ │ the user clicks "Sign in" → Cognito hosted UI loads │ │ • Email + password challenge │ │ • TOTP code challenge from authenticator app │ │ • Cognito issues JWT (15-minute access token, 1-hour refresh) │ └────────────────────────────┬─────────────────────────────────────────────────┘ │ (4) JWT returned to browser │ Browser caches in localStorage ▼ ┌──────────────────────────────────────────────────────────────────────────────┐ │ Browser JS calls /api/operators/health │ │ • Authorization: Bearer │ └────────────────────────────┬─────────────────────────────────────────────────┘ │ (5) CloudFront forwards to Lambda Function URL ▼ ┌──────────────────────────────────────────────────────────────────────────────┐ │ Lambda: vibecode-obs-backend │ │ • Validates JWT against Cognito public keys (cached) │ │ • Cache check: DDB ai-sandbox-agent-state partition session_id="obs-app" │ │ → if hit (<60s old): return cached JSON │ │ → if miss: continue │ │ • CloudWatch GetMetricData calls (Bedrock invocations, NFW packets, …) │ │ • CloudWatch Logs StartQuery for recent NFW alerts │ │ • Cost Explorer GetCostAndUsage for monthly spend │ │ • Aggregate, transform, return JSON │ │ • Write back to DDB cache with TTL+60s │ └────────────────────────────┬─────────────────────────────────────────────────┘ │ (6) JSON response ▼ ┌──────────────────────────────────────────────────────────────────────────────┐ │ Browser renders panels │ │ • Auto-refresh every 60 seconds for the Operators tab │ │ • Manual refresh for Exec/Audit (slower-changing data) │ └──────────────────────────────────────────────────────────────────────────────┘

Every call leaves a trail

So the "the user opened the dashboard" event itself is part of the audit trail the dashboard displays. Recursive observability in literal form.

4. Cost — every dollar tracked

Assumptions

Cost breakdown

AWS ServiceWhat it's billed forUnit costVolumeMonthly
S3 (static hosting) Storage + GET requests $0.023/GB-mo + $0.0004/1k GET 5 MB stored, 100k GETs (cached) $0.04
CloudFront Data transfer + requests $0.085/GB + $0.0075/10k req (US) ~5 GB out, 100k requests $0.50
ACM certificate TLS cert for custom domain 1 cert Free
Route 53 hosted zone DNS for obs.ai-sandbox.paycargo.com $0.50/zone/mo + $0.40/M queries 1 zone, ~10k queries $0.50
Subtotal: frontend delivery$1.04
Cognito User Pool Active users (MAU) $0.0055/MAU after 50k free tier ~10 MAU Free (within tier)
Cognito TOTP MFA All sign-ins Free
Subtotal: auth$0.00
Lambda backend Requests + GB-seconds $0.20/M req + $0.0000133/GB-sec (Graviton) 45k req, ~500ms × 1 GB avg $0.31
Lambda Function URL Free
DDB on-demand reads Read request units (eventually consistent) $0.25/M RRU ~45k reads $0.01
DDB on-demand writes Write request units $1.25/M WRU ~300 writes (cache+narrative) $0.001
Subtotal: API + cache$0.32
EventBridge scheduled rule Rule invocations $1.00/M events 30 events/mo Free
Bedrock (narrative) Tokens (Claude Haiku 4.5) $0.25/M input, $1.25/M output 90k input, 18k output tokens/mo $0.05
Bedrock Guardrails Guardrail invocations $0.075/1k text units ~30 invocations Negligible
Subtotal: scheduled + AI$0.05
CloudWatch metrics API (read) GetMetricData calls $0.01/1k metrics queried ~30k metrics queried/mo $0.30
CloudWatch Logs Insights queries Data scanned $0.005/GB scanned ~5 GB/mo $0.03
Cost Explorer API Cost & usage report calls $0.01/call ~30 calls/mo $0.30
Lambda audit log writes CloudWatch log ingestion $0.50/GB ingested ~50 MB $0.03
Subtotal: data reads + audit$0.66
Total monthly run cost (steady state, 10 users)~$2.07
Rounded planning estimate (with headroom)$5-10/month

Why so cheap

Everything is serverless and idle-zero. When nobody is using the app: zero Lambda invocations, zero DDB requests, zero Bedrock tokens, zero CloudFront traffic (CDN cache serves the cached HTML). Only S3 storage (pennies) and Route 53 ($0.50/mo) bill regardless. The app effectively turns off when nobody is looking at it. A traditional Fargate-based dashboard with a 0.5 vCPU container running 24/7 would cost ~$30/month just to exist, plus an RDS instance at $15/month minimum.

Cost scaling — what happens with more users

UsersMonthly costBottleneck
10 (today's plan)~$2-10None — everything is free-tier-adjacent
100 (Phase 2)~$15-25Lambda invocation count, CloudWatch query data scanned
1,000 (hypothetical product-mode)~$80-150CloudWatch metric API calls dominate; would refactor to push cached metrics into DDB on a schedule
10,000 (would never deploy this app at that scale)~$500-800Wholesale rearchitecture: CloudFront-side caching, dedicated metric ingest, possibly EventBridge fan-out

5. Auth — dual path with explicit cutover

Today (Phase 1): manual enrollment + TOTP MFA

Tomorrow (Phase 2): SSO federation via Identity Center

The cutover gate — two feature flags

# cartridges/administrative/obs/variables.tf
variable "enable_manual_enrollment" {
  description = "Allow Cognito-native email+password+TOTP signup. Flip to false once SSO migration is complete."
  type        = bool
  default     = true
}

variable "enable_sso_federation" {
  description = "Wire Identity Center as a SAML IdP. Flip to true once Identity Center is configured."
  type        = bool
  default     = false
}

Cutover steps:

  1. Identity Center configured (Phase 2 prerequisite work)
  2. Flip enable_sso_federation = true, apply — users can now sign in either way
  3. Communicate to all users: "Use the new SSO button from now on"
  4. Watch the obs dashboard's own auth panel — when manual sign-ins drop to zero, proceed
  5. Flip enable_manual_enrollment = false, apply — new manual signups blocked. Existing manual users can sign in but must migrate or be disabled.
  6. Optionally: 30 days later, run a script to delete manual user accounts that haven't signed in via SSO. Document deletion in the audit log.

Why dual path, not "wait for SSO"

Identity Center setup is a separate work stream that may take days or weeks to coordinate. The the external developer arrives next week. The exec demo to strategic leadership is imminent. We need the app accessible today, with a credible promise of "this will route through SSO once it's available." Dual path is the only honest way to make that promise.

6. The three audience tabs

Operators tab (platform operators, operations, the external developer)

Question this tab answers: "Is the platform healthy right now?"

Exec tab (CTO, VP Eng, CEO, CISO, strategic leadership, investors)

Question this tab answers: "What is the platform doing, what is it costing, is it safe?"

Audit tab (SecOps, compliance, external auditors)

Question this tab answers: "Who did what, when, on what data?"

7. The vibe: Bedrock-augmented daily narrative

Once a day at 09:00 ET, the Lambda backend's generateNarrative handler fires. It:

  1. Queries CloudWatch + Cost Explorer for the last 24h of platform data (invocations, cost, alert counts, error counts)
  2. Formats a structured prompt: "You are the AI platform's daily narrator. Yesterday's data: {data}. Generate a 2-paragraph executive summary suitable for CTO + CEO + investors. Mention notable events. Be honest about anomalies. Don't editorialize."
  3. Calls Bedrock InvokeModel with Claude Haiku 4.5 + the default platform guardrail
  4. Receives ~400-800 token response
  5. Writes to DDB cache with key session_id=obs-app, item_key=narrative-{date}
  6. Exec tab reads the latest narrative on load

Example output:

Yesterday the AI platform processed 1,247 agent invocations across 2 active tiers, returning 542k output tokens at a measured cost of $4.31. Activity was steady through the day with a peak around 14:00 ET coinciding with the weekly Sales report generation.

Three notable events: (1) one NFW alert fired against api.example-test.com at 11:34, investigated and confirmed as a the external developer integration test from a documented sandbox flow; (2) GuardDuty reported one Low severity finding regarding console-MFA-disabled, which applies to AWS Identity Center and is by design (Identity Center handles MFA at its layer, not at the AWS console layer); (3) the daily backup of the agent state table completed in 11 seconds. No PII workflows were exercised. No deploys.

The narrative is generated by an agent, against the same Bedrock + guardrail every other agent uses. It is itself an agent invocation that appears in the Bedrock invocation log group, that this very dashboard displays. The recursion is intentional and documented.

8. Deployment — CI through OIDC, same path as substrate

The vibe app deploys via the same CI workflow (Step 3a) that deploys the substrate. There is no separate pipeline.

The Terraform module

cartridges/administrative/obs/
├── versions.tf
├── variables.tf       (5 inputs: substrate references + feature flags)
├── frontend.tf        (S3 bucket, OAI, CloudFront, ACM cert, Route 53)
├── auth.tf            (Cognito User Pool, manual enrollment, SSO IdP attachment)
├── backend.tf         (Lambda function, Function URL, IAM role + policies)
├── eventbridge.tf     (daily schedule)
├── outputs.tf         (URL, Cognito Pool ID, Lambda ARN — for downstream apps)
├── lambda/
│   ├── package.json
│   ├── handler.js     (route handlers + cache logic)
│   └── narrative.js   (Bedrock invocation for daily summary)
└── frontend/
    ├── index.html
    ├── operators.html
    ├── exec.html
    ├── audit.html
    ├── app.js         (Cognito client + fetch wrapper + render)
    └── styles.css     (matches Sales mockup visual language)

Deploy flow

  1. Developer commits change in a feature branch
  2. PR opens → CI plan workflow runs → posts plan as PR comment
  3. Reviewer approves PR → merge to main
  4. Operator manually triggers terraform-apply.yml with confirm=APPLY
  5. CI assumes ai-sandbox-github-actions-deploy via OIDC, applies the change
  6. Static asset deploys: CI uploads new HTML/CSS/JS to the S3 bucket; CloudFront invalidation runs for /index.html and /*.html only (not /static/* which is content-hashed)
  7. Lambda code deploys: CI packages lambda/ into a zip, updates the function
  8. Done. New URL serves new content within ~60 seconds.

9. The app watches itself — recursive observability

Every component of the vibe app produces logs that the vibe app displays. The recursion is deliberate.

What the app doesWhere it shows up in the app
A user signs in (Cognito event)Audit tab → "Recent role assumptions" (Cognito uses AssumeRole under the hood)
The Lambda backend invokes a CloudWatch queryOperators tab → Lambda invocation count goes up by 1
The Bedrock narrative is generatedExec tab → Bedrock invocations chart shows +1; the narrative panel itself displays the output
Cost Explorer is queriedExec tab → MTD cost includes the cost of the Cost Explorer query itself
A failed sign-in (e.g., wrong TOTP code)Audit tab → Cognito failed sign-in event in CloudTrail; Operators tab → Lambda errors count if the JS triggers a 4xx

This is a useful property for two reasons. First, it proves the substrate is honest — there are no "magic" data sources that aren't real AWS resources. Second, it means the app is self-debugging: if the dashboard isn't updating, the very dashboard tells you why (Lambda errors, throttles, KMS denies).

10. Why serverless, not Fargate

You asked specifically about Fargate. The choice not to use it is deliberate and worth documenting.

ConcernFargate-based designServerless design (this)
Standing cost when idle~$30-50/month (smallest task running 24/7) + RDS minimum $15$0 idle; only S3 storage (cents) and Route 53 ($0.50)
Cold start latencyNone (always running)~400ms first call, then warm for ~10 min
ScalingConfigure ASG / target trackingAutomatic, no config
PatchingOperator updates base image, redeploysAWS handles runtime patching
VPC attachmentRequired for RDS access (adds ENI overhead)Not needed; reads AWS APIs from default Lambda network
Audit complexityContainer logs separate from app logsLambda invocation + app logs in one CloudWatch group
"Stop the app" costStop the task, RDS still billsJust stop deploying; everything goes to zero

The cold start trade-off is real. For an internal app that 10 people use occasionally, a 400ms cold start once per visit is invisible. For a customer-facing app with strict SLAs, the calculus is different and Fargate (or Lambda with provisioned concurrency, ~$5/month per concurrency unit) might win.

Documented in MODULES.md under "What we DON'T have, deliberately" so this choice doesn't get reconsidered every quarter.

11. How this proves the substrate doctrine

The observability vibe app is a working proof of every doctrine the substrate makes structural. A tech leader walking through it should see each property enforced in code, not in policy.

DoctrineHow the obs app proves it
Every action is attributable to a named humanAudit tab → AssumeRole events show mtang@paycargo.com + the Cognito principal for every sign-in
No long-lived AWS credentials anywhereApp uses Cognito JWTs (1 hour); Lambda uses its execution role (rotating with IAM); CI uses OIDC. No long-lived keys anywhere visible. Show the empty ~/.aws/credentials.
Audit logging cannot be silencedShow the CloudTrail status panel: "Logging: true" — and the permission boundary that prevents it from being stopped. The 5-PR substrate apply saga is the empirical proof.
Vibe apps inherit guardrail enforcement automaticallyThe narrative generation goes through the same default guardrail every other Bedrock call does. Show the Bedrock invocation log entry for the daily narrative — guardrail ID is present.
Encryption at rest is structural, not optionalShow the S3 bucket properties: SSE-KMS with audit CMK. Show the DDB encryption status. Show the CloudWatch log group encryption. All three reference the audit CMK from identity.
Audit retention meets complianceShow the audit S3 bucket Object Lock configuration: COMPLIANCE mode, 2555 days. Even AWS root cannot delete within retention.
Every code change is reviewableClick "Last successful deploy" on the Exec tab → opens the GitHub workflow run → shows the PR + the approver + the apply output.

12. Documentation we have delivered (and where this fits)

The vibe app does not stand alone. It consumes substrate built in Steps 0-4, follows the doctrine in MODULES.md, and serves the audiences identified in the 60-day plan. Tech leaders reviewing the app should have the full doc trail available.

13. Click-through prototype

An interactive mock of the obs vibe app lives at Step.4.5.clickthrough.html. It demonstrates:

The click-through is the artifact you can hand to strategic leadership or an investor in advance of a demo, so they can explore at their own pace. It is not a working app; it is a high-fidelity prototype for stakeholder review.

14. Forward reference — data governance CMK grid (Step 4.5.a)

The first deploy of this app hit a CloudFront 403 AccessDenied because the audit substrate CMK didn't grant CloudFront permission to decrypt the frontend S3 objects. The fix was not to broaden the audit CMK (which would dilute the most important key in the substrate). The fix was to recognize that application data has its own classification axis, distinct from substrate trust zones, and to add a grid of CMKs that the substrate provides and vibe apps consume by name.

The pattern (sensitivity tier × organizational boundary), the policy templates per tier, the cross-org access pattern ("higher audience as natural owner; lower audience via IAM grant"), and the per-vibe-app onboarding flow are all documented in Step 4.5.a — Data Governance CMKs. The obs app's frontend bucket now lands on ceo-office-low-risk; the Lambda + log group land on eng-low-risk. Every future vibe app inherits the pattern by declaring its governance cell.