● Foundation · executive view · 2026-08-07

The backbone
everything else sits on.

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.

1AWS account
1App core · Hub v2
1Orchestrator · Maestro
1Gate · Governor
0VPS in the loop
Scroll · The four pillars
The pillars

Four pieces. No exceptions.

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.

Pillar 01

AWS · the platform

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.

live Hub v2 cluster running

Pillar 02

Hub v2 · the app core

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.

live hv-hub-v2-{api,web}

Pillar 03

Maestro · the orchestrator

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.

live SQS · Lambda · DynamoDB · ADR 0001

Pillar 04

SSOT · brain + store

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.

live appUDQ65M1lSnSM5p + DynamoDB

Rule of thumb

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.

AWS stack

What we’re standing on

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.

HV Hub v2 AWS architecture — Route 53, ALB, ECS Fargate, the Maestro Lambdas, SQS, DynamoDB, S3 and Bedrock AgentCore in us-east-2
Animated · interactive — the AWS services this stands on (us-east-2, account 891377221201). Open interactive ↗
HV Hub v2 system architecture
Animated · interactive — the three planes (the Hub, the Maestro sales pipeline, Hermes) over one DynamoDB store, all inside AWS us-east-2. Open full ↗
AWS services · the short list

Each service, in one line

ServiceWhy we use itOwner pillarStatus
ECS FargateRuns every long-lived process (Hub v2 API, Web, maestro-core, Hermes) without us managing serversAWSlive
ECRStores the container images we ship · one repo per serviceAWSlive
ALBPublic HTTPS entry point · routes to ECS services by host/pathAWSlive
DynamoDBThe task store · single-table hv-hub-v2-development-tasks (TASK / GOVDECISION / TOUCH / CONFIG) · PutItem idempotency, Streams for the audit loop · single-digit ms, serverlessAWSlive
SQSDurable bus for Maestro generation · tasks-ingest + DLQ (maxReceive 5) between the producer and consumer Lambdas · resume-where-left-off, zero lossAWSlive
LambdaThe serverless generation + send pipeline · producer (/generate), task_consumer (Bedrock draft + Governor gate), message-dispatcher (the Unified Sender — email / Slack / WhatsApp) · $0 idle, auto-scalingAWSlive
Secrets ManagerEvery API key, every Basic auth password, every webhook secret · no more URL-embedded credsAWSlive
CloudWatchOne pane of glass for logs, metrics, alarms → SNS → Slack #hv-alertsAWSlive
S3Static docs site (this page), exported reports, image assetsAWSlive
EventBridgePer-job Hermes cron schedules + the daily digest · (the maestro_sender 30-min tick is currently disabled)AWSlive
Route 53 + ACMDNS for happen.ventures and subdomains · TLS certs auto-renewedAWSlive
IAMPer-service roles · no shared keys · ECS task roles instead of long-lived credentialsAWStightening
Auth.js v5 (NextAuth)Google OAuth · @happenventures.com only · httpOnly JWE session cookie (shared AUTH_SECRET) trusted by Hub v2 web + api — no CognitoHub v2live
CodeBuild + GitHub ActionsSelf-hosted GHA runners on CodeBuild · push to main → ECR → ECS rollout · Terragrunt CD for infraAWSlive
VPCCompute on public subnets (assign-public-IP) · ALB out front · egress via IGW · private subnets reserved, not yet in useAWSlive
BedrockManaged 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 modelsAWSlive

Why this list and not more

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.

Data · one store, two homes

DynamoDB for tasks, Airtable for the brain

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.

StoreModelWhat it holdsConcrete HV example
DynamoDBSingle-table key-value · PK/SK + GSI1 + StreamsEvery task and decision · item-types TASK / GOVDECISION / TOUCH / CONFIG · idempotent on the contact dedup idhv-hub-v2-development-tasks · pending_review drafts · the Governor touch log (1 / 14 days, 2 / 30 days) · Audit_Log via Streams
AirtableHuman-editable bases · the SSOT brainPlaybooks & cadence, guardrails, suppression, templates, the Status kill-switch, Agent SettingsBot Settings recK11xdc82ArNEX2 (Gabrielle) · Workflows & Automations SSOT · base appUDQ65M1lSnSM5p

Selection rule

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.

Auth · single sign-on

Auth.js v5 + Google · one login for the whole stack

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.

Step 01

User hits Hub v2 web

Next.js middleware redirects unauthenticated requests to /login, which kicks off the NextAuth Google flow. No custom login form to maintain.

Step 02

Auth.js brokers Google

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.

Step 03

JWE cookie trusted by the API

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.

ComponentResponsibilityOwns
Auth.js v5 (NextAuth)Brokers Google OAuth · issues + signs the httpOnly JWE session cookie (shared AUTH_SECRET)Hub v2
Google Workspace IdPCredential check · MFA · account lifecycle (offboarding propagates)Google
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 routeHub v2
Maestro pipelineService-to-service: shared QUEUE_HOOK_SECRET bearer header on /ingest, /approved, /:id/status · Lambda IAM task roles for AWS callsMaestro

Why Auth.js + Google and not Cognito / Auth0 / Clerk

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.

What the foundation unlocks

Once we agree, this is what we get

For Jessica

Predictable shipping

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.

For Farid

One architecture story

The same diagram explains every workload. New hires onboard in days. Reviews focus on business logic, not infra plumbing.

For the team

No more 3 AM VPS

Multi-AZ Fargate · DynamoDB point-in-time recovery · CloudWatch alarms · rotation policies. The on-call playbook becomes “check the dashboard”, not “SSH and tail”.

For finance

One AWS bill

Everything tagged by service. Cost attribution per pillar. No more “what is this $40/mo on the VPS host?”.

For sales

Compliance answers

SOC 2 / GDPR conversations get a real answer: AWS-hosted, IAM-scoped, encrypted at rest and in transit, audit log in CloudWatch.

For Robin

One review surface

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.

The cutover, in four moves

How we got from the VPS to the foundation

01
Store + bus · done
Stand up the DynamoDB single-table and the SQS tasks-ingest queue + DLQ. ADR 0001 chose DynamoDB over Postgres and the SQS producer/consumer path over a synchronous ECS call.
DynamoDB · SQS · ADR 0001
02
Generation re-pointed · done
The n8n “Auto Tasks” crons stopped POSTing the VPS and now hit /generate → producer Lambda → SQS → task_consumer (Bedrock). 1,202 Airtable rows backfilled into DynamoDB at exact parity.
11 generator crons · producer + consumer Lambda
03
Review + send live · done
Reps review in the Hub /queue (read side is AWS-only). Approve & Send is un-gated (#222); the maestro_sender Lambda delivers via Gmail. robin_tasks cut over to the same store (#259).
Hub v2 /queue · maestro_sender · #222 / #259
04
Governor live · VPS retired · done
The Contact Governor (GOV-01) gates every task-write with ALLOW / DEFER / BLOCK / DROP, and the VPS has been retired — generation now runs 100% on the AWS consumer (#152). The one item left is send consolidation: a legacy n8n send-worker still polls the approved queue and must be retired before real send is enabled.
GOV-01 · #152 · dual-send-worker retirement

⚠️ One open hazard

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.

Decisions locked

The calls that settled the foundation

Three scope-and-ownership decisions, all now agreed and shipped. They are what keep the contract honest.

Decision 01

AWS is the only host

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).

Decision 02

DynamoDB is the task store

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.

Decision 03

The Governor is the gate

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.