Before we ship more bots, more playbooks or more surfaces, we agree on the foundation. One AWS account, one application core (Hub v2), one orchestrator (Maestro) and one source of truth — Airtable for the brain (playbooks, guardrails, kill-switch), DynamoDB for the task store. Every future feature plugs into these — nothing else.
If a new initiative doesn’t map cleanly onto one of these four, it belongs in a discovery doc — not in production. This is the contract Jessica and Farid asked us to draw before we go deeper.
One account (891377221201) in us-east-2. Every workload runs on managed primitives: ECS Fargate, DynamoDB, SQS, Lambda, Bedrock, Secrets Manager, CloudWatch, S3, ALB, IAM, VPC. No more SSH-into-a-VPS.
NestJS API + Next.js 15 web on ECS Fargate at hubv2.apphappenventures.com. Hosts the /queue review surface (the tasks-v2 HITL queue) and the team-facing endpoints. One deploy pipeline, one auth model.
The AWS-native engine that replaced the ClawBot VPS. n8n crons POST /generate → producer Lambda → SQS → task_consumer Lambda (Bedrock draft) → DynamoDB. Generation runs serverless; apps/maestro-core on ECS is the scaffold / ACK stub (idle, desired=0) — drafting lands in #131; the serverless path does the real generation.
Split source of truth. Airtable holds the brain — playbooks, guardrails, suppression, the kill-switch and Agent Settings. DynamoDB (single-table hv-hub-v2-development-tasks) holds every task and decision. Task data is AWS-only; the config stays where humans edit it.
If a request can’t be answered with “it’s an AWS service, a Hub v2 endpoint, a Maestro playbook or an Airtable / DynamoDB row” — we’re inventing a fifth pillar. That’s the moment to stop and talk.
The AWS services that make up the foundation, grouped by what they do for us. No experimental services, no exotic regions — only the boring, well-understood primitives.
| Service | Why we use it | Owner pillar | Status |
|---|---|---|---|
| ECS Fargate | Runs every long-lived process (Hub v2 API, Web, maestro-core, Hermes) without us managing servers | AWS | live |
| ECR | Stores the container images we ship · one repo per service | AWS | live |
| ALB | Public HTTPS entry point · routes to ECS services by host/path | AWS | live |
| DynamoDB | The task store · single-table hv-hub-v2-development-tasks (TASK / GOVDECISION / TOUCH / CONFIG) · PutItem idempotency, Streams for the audit loop · single-digit ms, serverless | AWS | live |
| SQS | Durable bus for Maestro generation · tasks-ingest + DLQ (maxReceive 5) between the producer and consumer Lambdas · resume-where-left-off, zero loss | AWS | live |
| Lambda | The serverless generation + send pipeline · producer (/generate), task_consumer (Bedrock draft + Governor gate), message-dispatcher (the Unified Sender — email / Slack / WhatsApp) · $0 idle, auto-scaling | AWS | live |
| Secrets Manager | Every API key, every Basic auth password, every webhook secret · no more URL-embedded creds | AWS | live |
| CloudWatch | One pane of glass for logs, metrics, alarms → SNS → Slack #hv-alerts | AWS | live |
| S3 | Static docs site (this page), exported reports, image assets | AWS | live |
| EventBridge | Per-job Hermes cron schedules + the daily digest · (the maestro_sender 30-min tick is currently disabled) | AWS | live |
| Route 53 + ACM | DNS for happen.ventures and subdomains · TLS certs auto-renewed | AWS | live |
| IAM | Per-service roles · no shared keys · ECS task roles instead of long-lived credentials | AWS | tightening |
| Auth.js v5 (NextAuth) | Google OAuth · @happenventures.com only · httpOnly JWE session cookie (shared AUTH_SECRET) trusted by Hub v2 web + api — no Cognito | Hub v2 | live |
| CodeBuild + GitHub Actions | Self-hosted GHA runners on CodeBuild · push to main → ECR → ECS rollout · Terragrunt CD for infra | AWS | live |
| VPC | Compute on public subnets (assign-public-IP) · ALB out front · egress via IGW · private subnets reserved, not yet in use | AWS | live |
| Bedrock | Managed LLMs · Claude Sonnet 4.6 (us.anthropic.claude-sonnet-4-6) drafts Maestro’s outreach (consumer Converse) and powers the Hub’s Edit-with-AI rewrite + chat (SSE). Also hosts Maestro (ECS) and Hermes (AgentCore Runtime). IAM scoped to bedrock:InvokeModel* on Anthropic models | AWS | live |
We deliberately do not use Step Functions, App Runner, EKS, Aurora or OpenSearch at the foundation level. They’re fine services — they’re just not in the contract. Adding one means a discussion, not a Jira ticket.
The cutover settled the data question (ADR 0001): the task store is DynamoDB, the config stays in Airtable. No Postgres at the foundation — RDS lives only in the separate hv-data-platform.
| Store | Model | What it holds | Concrete HV example |
|---|---|---|---|
| DynamoDB | Single-table key-value · PK/SK + GSI1 + Streams | Every task and decision · item-types TASK / GOVDECISION / TOUCH / CONFIG · idempotent on the contact dedup id | hv-hub-v2-development-tasks · pending_review drafts · the Governor touch log (1 / 14 days, 2 / 30 days) · Audit_Log via Streams |
| Airtable | Human-editable bases · the SSOT brain | Playbooks & cadence, guardrails, suppression, templates, the Status kill-switch, Agent Settings | Bot Settings recK11xdc82ArNEX2 (Gabrielle) · Workflows & Automations SSOT · base appUDQ65M1lSnSM5p |
Is it task or decision state a bot writes at runtime? → DynamoDB (AWS-only — nothing reads or writes tasks to Airtable anymore). Is it config a human edits — a playbook, a guardrail, a kill-switch? → Airtable.
No more per-app passwords. Auth.js v5 (NextAuth) brokers Google OAuth directly — no Cognito. Hub v2 web and Hub v2 API are same-origin and share one AUTH_SECRET, so they trust the same session cookie.
Next.js middleware redirects unauthenticated requests to /login, which kicks off the NextAuth Google flow. No custom login form to maintain.
NextAuth runs the Google OAuth flow directly (callback /auth/callback/google). The user signs in with their @happenventures.com account · MFA stays Google’s problem.
NextAuth sets an httpOnly JWE session cookie signed with the shared AUTH_SECRET. Same-origin, the browser auto-sends it to /api; Nest decodes it (readVerifiedIdentity) — no token forwarding, no JWKS.
| Component | Responsibility | Owns |
|---|---|---|
| Auth.js v5 (NextAuth) | Brokers Google OAuth · issues + signs the httpOnly JWE session cookie (shared AUTH_SECRET) | Hub v2 |
| Google Workspace IdP | Credential check · MFA · account lifecycle (offboarding propagates) | |
| Hub v2 web (Next.js) | NextAuth flow + middleware gate · session cookie storage (httpOnly) | Hub v2 |
| Hub v2 API (NestJS) | Decodes the session cookie with the shared AUTH_SECRET (readVerifiedIdentity) · RBAC (roles / modules) on every route | Hub v2 |
| Maestro pipeline | Service-to-service: shared QUEUE_HOOK_SECRET bearer header on /ingest, /approved, /:id/status · Lambda IAM task roles for AWS calls | Maestro |
Auth.js v5 brokers Google OAuth directly with zero extra infrastructure — no user pool, no Hosted UI, no JWKS round-trip. Web and API are same-origin and share one AUTH_SECRET, so the API just decodes the httpOnly JWE cookie. One fewer service in the AWS bill and one fewer secret to rotate.
Every new bot, playbook or surface follows the same path: ECS / Lambda deploy · secret in Secrets Manager · playbook row in Airtable · tasks in DynamoDB. No bespoke infra per feature.
The same diagram explains every workload. New hires onboard in days. Reviews focus on business logic, not infra plumbing.
Multi-AZ Fargate · DynamoDB point-in-time recovery · CloudWatch alarms · rotation policies. The on-call playbook becomes “check the dashboard”, not “SSH and tail”.
Everything tagged by service. Cost attribution per pillar. No more “what is this $40/mo on the VPS host?”.
SOC 2 / GDPR conversations get a real answer: AWS-hosted, IAM-scoped, encrypted at rest and in transit, audit log in CloudWatch.
Robin’s tasks now flow through the same DynamoDB store and the Hub /queue as sales — one HITL queue, one action set, source tells the bots apart.
A legacy n8n “AWS Send Worker (Maestro)” still polls the approved queue alongside the maestro_sender Lambda. Both run in dry-run today, so no client is contacted — but the n8n worker must be retired before real send is enabled, or the same approved task could be sent twice.
Three scope-and-ownership decisions, all now agreed and shipped. They are what keep the contract honest.
No more side-deploys to Render, Railway, Heroku or a friend’s VPS. If it needs to run, it runs on ECS Fargate or Lambda in us-east-2. The VPS has been retired (#152).
ADR 0001: the bot HITL queue lives in the DynamoDB single-table, ingested via the SQS producer/consumer path — not Postgres, not a synchronous ECS call. Task data is AWS-only.
Every playbook calls the Contact Governor (GOV-01) before it writes a task. It owns “should we contact this person, and how often” — frequency caps, families, the touch log. Playbooks keep only their list + cadence.