Think2earn · Architecture · verified 2026-07-08 · page updated 2026-07-09

Production architecture with Fargate canary

The product still fronts on one EC2 control plane. Agent-mode turns now have a second, live worker plane: throwaway Fargate tasks with per-turn IAM, rolled out behind a temporary per-user allowlist. Everyone else still hits AgentCore.

Production default Live Fargate canary Next cutover / north star

At a glance

Verified against live prod (chat.think2earn.com), EC2 i-076d244b62ec4d8e8 (think2earn-prod), account 995772916887, region us-east-1, and stack think2earn-harness (handoff 2026-07-08; E2E 33/33, isolation probes 18/18).

stack UPDATE_COMPLETE canary allowlist ON global runtime = agentcore dispatch-enabled = true EC2 still owns API + SPA
Layer Production default Live canary (allowlisted) Next / not yet
Public URL chat.think2earn.com (Cloudflare Tunnel → EC2 :8080) Static SPA on CDN + API split
Control plane Single EC2: Node/Express API + Vite SPA, billing, journal owner Serverless API (Lambda); retire EC2 for app traffic
Default chat (chatMode: simple) Bedrock ConverseStream tool loop on EC2; AgentCore Browser / CI / Memory / Exa Gateway Same loop, eventually off EC2
Agent-mode turns AgentCore Runtime SSE path on EC2 (byte-identical for non-allowlisted users) Fargate async: 202 + journal poll; one ECS task per turn Global THINK2EARN_HARNESS_RUNTIME=fargate; decommission AgentCore chat path
Isolation model App-layer tenancy; shared Think2earnHostRole for AgentCore Per-turn vended IAM scoped to users/<userId>/… + turn keys All agent turns on Fargate isolation
Turn journal In-memory hot path on EC2 + optional DynamoDB table think2earn_turn_journal (Fargate path writes here) Journal fully durable for all modes; EC2 memory not required
Dispatch / workers Live: SQS kick, 4 Lambdas, ECS Fargate taskdef think2earn-harness-claude-sdk:4 SOCI + slim image; optional warm linger
Auth / billing Cognito + native auth + app JWT; Stripe credits — unchanged
Deploy App: PR → main → deploy.yml (SSM + rsync) or deploy-via-ssm.sh. Env file excluded from rsync. Harness: sam deploy + ECR image tag = git SHA

Default Production control plane

One long-lived EC2 host still fronts the entire web product. The Fargate plane does not replace the API — it only executes allowlisted agent-mode turns.

flowchart TB
  subgraph users [Users]
    Browser[Browser]
  end

  subgraph edge [Cloudflare]
    CF_DNS[DNS]
    CF_Tunnel[Tunnel to origin]
  end

  subgraph ec2 [Control plane EC2 think2earn-prod]
    Cloudflared[cloudflared]
    NodeApp[Node Express API plus Vite SPA]
    Journal[Turn journal memory plus DDB]
    Router[effectiveHarnessRuntime]
    IAMHost[Think2earnHostRole]
  end

  subgraph agentcore_path [Agent path default]
    Runtime[AgentCore Runtime microVM]
  end

  subgraph fargate_path [Agent path canary]
    Kick[SQS turn-kick]
    Dispatch[dispatch-turn Lambda]
    Task[ECS Fargate worker]
    Vend[vend-credentials Lambda]
    Finalize[finalize-turn Lambda]
  end

  subgraph tools [Shared tool services]
    Converse[Bedrock ConverseStream]
    CI[Code Interpreter]
    BrowserAC[Browser CDP]
    Memory[AgentCore Memory]
    Gateway[Exa Gateway]
  end

  subgraph data [AWS data]
    DDB[(DynamoDB core plus journal plus dispatch)]
    S3[(S3 artifacts and staging)]
  end

  Browser --> CF_DNS --> CF_Tunnel --> Cloudflared --> NodeApp
  NodeApp --> Journal
  NodeApp --> Router
  NodeApp --> DDB
  NodeApp --> S3
  NodeApp --> IAMHost
  IAMHost --> Converse
  IAMHost --> CI
  IAMHost --> BrowserAC
  IAMHost --> Memory
  IAMHost --> Gateway
  Router -->|not allowlisted agent mode| Runtime
  Router -->|allowlisted agent mode| Kick --> Dispatch --> Task
  Task --> Vend
  Task --> Journal
  Task --> S3
  Task --> Converse
  Task -.-> Finalize
      

Request paths that every user hits

Default chat — simple mode

  • chatMode defaults to simple
  • Browser POST → SSE on EC2
  • Bedrock ConverseStream + native tool loop (max tool rounds)
  • Tools when env IDs set: run_code, browse_web, web_search, update_plan
  • AgentCore Browser / CI / Memory / Gateway called from the host role
  • Screenshots and run_code files persisted to S3 as conversation artifacts
  • Does not use Fargate or the allowlist

Agent mode — non-allowlisted (majority)

  • THINK2EARN_HARNESS_RUNTIME=agentcore default
  • User not in THINK2EARN_FARGATE_USER_ALLOWLIST
  • SSE 200 path on EC2 via AgentCore Runtime / Claude Agent SDK runtime image
  • No dispatch row, no RunTask, no vend call
  • Flag-off regression proven: still byte-identical AgentCore SSE

Characteristics (still true)

Canary Fargate worker plane (live)

Option B from the Jul 2026 architecture decision is partially shipped: AgentCore is no longer the only agent executor. The worker plane runs each allowlisted agent turn in its own throwaway container; isolation is IAM math (vended session policy), not process trust.

What is live vs what is not

  • Live: SAM stack think2earn-harness, 4 Lambdas, SQS kick + DLQ, DynamoDB dispatch, ECR worker image, ECS cluster/taskdef, vend API, finalize + settlement, sweeper, host IAM patch, canary env flags.
  • Live for canary only: agent-mode POST /chats/:id/messages202 { mode: "fargate-async" } + client journal polling.
  • Not live: global Fargate cutover; EC2 retirement; SPA on CloudFront; full serverless control plane; SOCI-accelerated pulls (scaffolded, image still ~1365 MB without SOCI).

Lambdas (one job each)

Function Trigger Responsibility
dispatch-turn SQS think2earn-turn-kick Claim QUEUED → DISPATCHING (capacity: 3/user, 60 global), RunTaskRUNNING
vend-credentials HTTP API /internal/vend (SigV4, task identity) 6-step verify → AssumeRole with compact session policy + userId tag; refreshable ~50 min
finalize-turn EventBridge on ECS task STOP Terminal dispatch status (DONE / FAILED / CANCELLED) + settlement (assistant message, artifact dock rows)
sweeper 1/min schedule Kick queued, reconcile stale DISPATCHING/RUNNING vs ECS (5 min heartbeat), hard-stop cost cap

Worker container

Temporary Canary user allowlist

Routing is decided in the EC2 app by effectiveHarnessRuntime(userId) (src/config.ts). Global flag stays agentcore; only listed user IDs take Fargate.

Production canary config (verified on EC2 process environ)

  • THINK2EARN_HARNESS_RUNTIME=agentcore — default for everyone
  • THINK2EARN_FARGATE_USER_ALLOWLIST=user_d5418965-e879-492f-bb98-af64ec605893,e4787408-a061-7054-308a-b910301a96c6
  • Both IDs are the same person (Carol), not two different canary users:
    • user_d5418965-… — native-auth userId (carol@think2earn.com)
    • e4787408-… — Google-federated Cognito sub (acalincarol@gmail.com)
  • Matcher uses context.user.userId (native = user_<uuid>, Cognito = raw sub). Both must be listed if both login methods are used — fixed in PR #146 after #145 only listed the Cognito sub.
  • Mirrored in scripts/render-ec2-env.sh (env is not deployed by rsync).
flowchart TD
  POST["POST /chats/:id/messages agent mode"]
  Check{"effectiveHarnessRuntime userId"}
  Global{"HARNESS_RUNTIME = fargate?"}
  Allow{"userId in FARGATE_USER_ALLOWLIST?"}
  AC["AgentCore SSE path 200"]
  FG["startTurn: stage S3 + journal META + dispatch QUEUED + SQS kick"]
  R202["202 mode fargate-async"]
  Poll["Client polls GET .../turns/:requestId/events"]

  POST --> Check
  Check --> Global
  Global -->|yes| FG
  Global -->|no| Allow
  Allow -->|yes| FG
  Allow -->|no| AC
  FG --> R202 --> Poll
      

How to change / roll back the canary

Action How
Add a canary user Append both of their identities if they use native + federated login; render env; SSM push; restart service
Turn-level rollback Empty THINK2EARN_FARGATE_USER_ALLOWLIST; keep runtime agentcore; render + push + restart
Freeze all new Fargate work SSM /think2earn/dispatch-enabled=false
Global cutover (future) THINK2EARN_HARNESS_RUNTIME=fargate — allowlist becomes moot

Canary path Fargate turn lifecycle

sequenceDiagram
  participant C as Client
  participant E as EC2 app
  participant S3 as S3 staging
  participant J as turn_journal
  participant D as turn_dispatch
  participant Q as SQS kick
  participant L as dispatch-turn
  participant T as Fargate worker
  participant V as vend-credentials
  participant F as finalize-turn

  C->>E: POST messages agent mode allowlisted
  E->>S3: put input.json under users/userId
  E->>J: META runtime fargate
  E->>D: row QUEUED
  E->>Q: SendMessage kick
  E-->>C: 202 requestId mode fargate-async
  C->>J: poll events
  Q->>L: kick
  L->>D: claim QUEUED to DISPATCHING
  L->>T: RunTask
  L->>D: RUNNING
  T->>V: vend scoped creds
  V-->>T: temporary credentials
  T->>J: stream events artifacts done
  T->>S3: artifact sweep
  Note over T,F: task STOP EventBridge
  F->>D: DONE or FAILED or CANCELLED
  F->>E: settlement message plus dock rows
      

Cancel semantics

Capacity & safety

Compute paths compared

Workload Who Where it runs today
Quick chat + tools All users EC2 → Bedrock ConverseStream; AgentCore CI / Browser / Gateway from host
Agent turn Non-allowlisted EC2 SSE → AgentCore Runtime (Claude Agent SDK image / wave1 runtime)
Agent turn Allowlisted canary EC2 stages + 202 → Fargate worker + vend IAM → journal poll → finalize settlement
SSE / journal for simple mode All users EC2 holds stream; E8 continues after disconnect; in-memory journal (+ DDB when configured)
Fargate journal poll Canary agent turns Client polls journal events; journal done is authoritative; dispatch terminal lags ≤2 min
User shell / terminal Entitled workspaces Still host-side local_terminal / premium workspace path — not the Fargate worker

Data & state

State Store today Notes
Conversations, messages, users, billing DynamoDB (foundation stack tables) Unchanged entity model
Turn journal (SSE / poll replay) EC2 memory hot path + think2earn_turn_journal Fargate path requires journal table; Wave 0 dual-write design
Turn dispatch queue of record think2earn_turn_dispatch SQS is only a kick, not the queue of record
Fargate input staging S3 users/<userId>/… input.json Worker never reads conversations table
Artifacts (images, HTML, files, plans) S3 + library assets; dock via GET /artifacts Simple-mode screenshots + run_code files; Fargate settlement writes dock rows
Working plan conversation.workingPlan via update_plan tool No separate store entity
Secrets / flags SSM + EC2 env files; stack params dispatch-enabled in SSM; canary flags in env
Per-turn worker credentials STS session via vend Lambda Not in task env; refreshable; packed-policy budget guarded by tests

Live stack inventory

Resource Value
Stack think2earn-harness (us-east-1)
ECS cluster think2earn-harness-cluster
Task definition think2earn-harness-claude-sdk:4
ECR 995772916887.dkr.ecr.us-east-1.amazonaws.com/think2earn-harness-node
Kick queue https://sqs.us-east-1.amazonaws.com/995772916887/think2earn-turn-kick
Vend endpoint https://pmva7u24hc.execute-api.us-east-1.amazonaws.com/internal/vend
Dispatch table think2earn_turn_dispatch
Journal table think2earn_turn_journal
Host IAM patch Think2earnHostFargatePatch on Think2earnHostRole (dispatch, kick, staging PutObject)
Evidence GitHub issue #124 · handoff docs/handoff-fargate-worker-plane-2026-07-08.md · PRs #118–#154

Repo map

Worker plane

  • sam/template.yaml
  • sam/functions/{dispatch-turn,finalize-turn,sweeper,vend-credentials}/
  • deploy/harness-fargate/ (Dockerfile, worker, probes)

EC2 seam

  • src/harness/startTurn.ts
  • src/config.tseffectiveHarnessRuntime
  • Cancel / poll routes in src/http/api.ts
  • scripts/render-ec2-env.sh canary block

Next Roadmap from here

  1. Canary week (now) — real use on allowlisted Carol identities; measure kick→first-event latency, vend errors, DLQ, cost delta.
  2. W2-D image slimming + SOCI — baseline ~34–37 s to first journal event / ~46–51 s done (haiku); target ≤~30 s first event.
  3. Linger-window decision — whether follow-up turns reuse a warm task vs pay cold start every time.
  4. §14 step 7 global cutoverTHINK2EARN_HARNESS_RUNTIME=fargate; allowlist moot; then decommission AgentCore chat path.
  5. Later control-plane migration — SPA on CDN; API off EC2; Cloudflare Tunnel only until that split lands.

Stays on EC2 for now

  • Public API + SPA
  • Simple-mode Bedrock loop
  • Billing / Stripe / Cognito callbacks
  • Start-turn staging for Fargate

Already on serverless workers

  • Dispatch claim + RunTask
  • Credential vending
  • Finalize + settlement
  • Sweeper / capacity reconcile

Still north-star only

  • No always-on API host
  • All users on Fargate agents
  • pub.think2earn.com publish tier
  • Per-user IAM (deferred)

What changed vs the old architecture page (2026-06-30)

The previous page claimed a pure “current EC2 / target serverless AgentCore Runtime” split and said the turn journal was only in-memory. Several of those claims are now wrong or incomplete after the Fargate worker plane landed.

Agent execution is dual-path, not single-path

Old page: long work = AgentCore Runtime only; target was still AgentCore + Claude Agent SDK microVMs.
Now: decision Option B replaced that target for agent turns — ECS Fargate is the worker plane, live behind allowlist; AgentCore remains the default until global cutover.

Control plane is not serverless yet

Old page target: “no always-on API EC2” as the near-term north star drawing.
Now: EC2 remains the API/journal/settlement owner by design for this phase. Fargate only executes turns. Full serverless control plane is still later roadmap, not deployed.

Journal + dispatch tables exist

Old page: journal = EC2 memory ~30 min TTL only.
Now: dual-write journal with DynamoDB table; Fargate uses durable journal events + dispatch state machine as the queue of record (SQS is a kick only).

Credentials are vended, not static env

Workers hold near-zero permissions and call a vend Lambda for refreshable, per-user/per-turn STS sessions (fixes the 1-hour role-chaining trap called out in the design review).

Temporary canary allowlist is an explicit production control

Not mentioned on the old page. Production ships with Carol’s two login identities allowlisted; empty the list to roll back without tearing down the stack.

What stayed the same

Still out of scope for this phase