Two implementations, one architecture
We build tenwhy twice, in parallel. Implementation 1 wires every vendor by hand. Implementation 2 provisions the same vendors through the Stripe Projects ecosystem. The application code is identical between them — only the provisioning, credentials, and billing plane differs. Running both at once is the truest test that the architecture is portable and not accidentally welded to one set of vendors.
1 · The principle — the .env is the seam
Maestro, the specialists, the job contract, the brain client — none of them know which
implementation they're running in. They read DATABASE_URL,
OPENROUTER_API_KEY, SANDBOX_API_KEY, etc. from the environment.
The only difference between Impl 1 and Impl 2 is how those values land in
.env.
| Step | Impl 1 · hand-wired | Impl 2 · Stripe Projects |
|---|---|---|
| get a Postgres | create it in the Neon/Render dashboard, copy DATABASE_URL into .env |
stripe projects add neon/postgres → stripe projects env --pull |
| get an LLM key | paste OpenRouter key into .env |
stripe projects add openrouter → env --pull |
| get a sandbox | Sapiom/Blaxel API key into .env |
stripe projects add daytona → env --pull |
The app reads the same variable names either way. That makes the two implementations a controlled A/B: same code, two provenances.
2 · The proving loop (MVP)
The smallest thing that exercises the whole architecture: one task, one orchestrator, one specialist, one structured round trip, one durable audit. If this loop is green on both implementations, scaling to the full workforce is mechanical.
- A task arrives Customer types a request into a form (or curl). For the MVP there's one hard-coded test customer.
- Maestro reasons Maestro (LLM) reads the relevant brain docs, composes a brief, and decides to fire Jon (catalog).
- Jon runs Jon receives the job-contract request, reasons over the brief + brain snapshot (LLM), and emits a structured proposal — no external API in the MVP, just catalog reasoning.
-
Jon returns the contract
Jon emits job-contract response JSON:
status,actions[],brain_updates[],cost_usd, etc. -
Maestro persists
Maestro validates the contract, writes a row to
action_log, and appends an entry to/jon/history.htmlin the brain repo. - Customer sees the result A plain HTML page renders the brain + the action log. (Generative dashboard comes later.)
What the MVP deliberately cuts
| Cut | Why it's safe to defer |
|---|---|
| The software factory | Hand-write Jon. Proving the workforce loop matters first; the factory automates tool-building later. |
| 11 other specialists | One specialist proves the contract. The rest are the same shape. |
| Multi-tenancy | One hard-coded test customer + one seed brain. |
| Generative dashboard | Plain HTML renders brain + log. json-render comes once the loop is real. |
| Scheduler / shifts | Jon fires on demand only. |
| Real auth | Single hard-coded session for the MVP. |
| Scraping | Jon (catalog) doesn't scrape. The scraping service arrives with Max (SEO). |
kept in MVP Maestro does use an LLM from the start — it reads the brain, composes the brief, and decides. Even with one specialist, Maestro is a real orchestrator, not a dumb router.
What the MVP must prove — the failure paths
The happy-path round trip is the easy half. The MVP isn't green until it survives the failures the durable layer exists for (design §11) — these are test cases, not afterthoughts:
| Failure injected | Required behaviour |
|---|---|
| Job retried | Re-running re-applies nothing already landed — idempotency-key check against the action_items ledger. |
| Duplicate webhook | A re-delivered result is a no-op — inbox dedupe on unique(job_id, event, external_id). |
| Lost webhook | Reconciliation reads external truth back and closes the job; no effect lost or double-counted. |
| Crash before external apply | Job resumes from applying; nothing executed, so it executes cleanly. |
| Crash after apply, before record | Reconciliation finds the landed effect and finalizes the record — the hardest case, and the reason the ledger exists. |
| Projection rebuild | A query-hot projection can be dropped and rebuilt from results with identical output. |
| Brain-commit validation | A malformed proposed brain update is rejected at finalize; the canonical record is platform-rendered, not tool-written. |
3 · Component map — Impl 1 vs Impl 2
Rows marked muted are identical across both. The colored rows are where the two implementations genuinely diverge.
| Layer | Impl 1 · hand-wired | Impl 2 · Stripe Projects |
|---|---|---|
| Maestro host | Render web service (manual) | Render/Fly via projects add |
| Specialist sandbox | Sapiom / Blaxel µVM | Daytona (<90ms, TS SDK) |
| Database | Render Postgres (manual) | Neon via projects add |
| LLM gateway | OpenRouter | OpenRouter (same vendor) |
| Brain repo | GitHub (outside Projects) | GitHub (outside Projects) |
| Secrets | Infisical | Stripe Secret Store (built in) |
| Queue / background | Render Cron | Inngest via projects add |
| Object store | Cloudflare R2 | Cloudflare R2 via projects add |
| Auth | custom magic link + CF Email | WorkOS (AuthKit) |
| Scraping | Scrapling service (self-hosted, Python + Playwright) | Firecrawl via projects add |
| Observability | Sentry + Better Stack | Sentry (catalog) + Better Stack (manual) |
| DNS / CDN | Cloudflare | Cloudflare via projects add |
| Customer billing | Stripe API (wired by hand) | native Stripe (the platform's reason to exist) |
note Scraping is a service behind an HTTP boundary in both — Scrapling (self-hosted) is the open-source equivalent of Firecrawl (hosted). The Node tool code that calls it is the same; only the base URL + auth change. This keeps the µVM Node-only (decision #13) intact — Scrapling's Python stays inside its own service, never inside a tool µVM.
note The Queue / background row is the portability seam to watch. Render Cron (Impl 1) and Inngest (Impl 2) must deliver the same platform events to the same job-state-machine code (design §11), behind one scheduler/event interface — with a conformance suite both implementations pass. Otherwise the two impls quietly diverge exactly where it's hardest to see.
4 · The provisioning seam in practice
Same .env shape, two ways to fill it.
# Impl 1 — hand-wired: you paste each value DATABASE_URL=postgres://... # from Neon/Render dashboard OPENROUTER_API_KEY=sk-or-... # from openrouter.ai SANDBOX_API_KEY=... # from Sapiom/Blaxel GITHUB_TOKEN=ghp_... # brain repo PAT SCRAPER_URL=https://scrape.internal # self-hosted Scrapling # Impl 2 — Stripe Projects: provision, then pull stripe projects add neon/postgres stripe projects add openrouter stripe projects add daytona stripe projects add firecrawl stripe projects env --pull # writes the same .env keys stripe projects billing update --limit 200 --provider daytona
The last line is a real win: per-provider spend caps from one command directly address the cost-runaway item on the design's "still on the grill" list.
5 · Repo layout — one repo, shared code
Single repository. The app is shared; only thin per-implementation provisioning configs differ.
tenwhy/ ├── app/ # shared — identical across impls │ ├── maestro/ # orchestrator (LLM): reads brain, composes brief, fires specialists │ ├── specialists/ │ │ └── jon/ # catalog specialist (MVP); later extracted to tenwhy-tools/ │ ├── contract/ # job-contract v0 types + validator │ ├── brain/ # GitHub brain client (read docs, append history.html) │ └── web/ # MVP: plain HTML render of brain + action_log ├── infra/ │ ├── impl1/ # Render blueprint, Sapiom config, Scrapling service │ └── impl2/ # .projects/ state, Stripe Projects manifest ├── seed/ │ └── brain/ # hand-authored sample brain for the test customer └── AGENTS.md # orients agents to the doc set + this plan
6 · Build phases
Ordered, not timed. Each phase ships on both implementations before the next begins — that's what keeps them honest.
-
The loop, in-process
Maestro + Jon in one process, no sandbox yet. Job contract v0. Postgres
action_log+ brainhistory.html. Plain HTML view. Deployed on both. both · proves the round trip - Add the sandbox Jon moves into a sandbox — the first real divergence. Impl 1 = Sapiom/Blaxel · Impl 2 = Daytona. Same job-contract wire on both. impl 1: Sapiom · impl 2: Daytona
- Second specialist + Maestro routing Add Max (SEO) → Maestro now genuinely chooses between specialists. Brings in the scraping service: Scrapling (Impl 1) / Firecrawl (Impl 2). both · Maestro selection logic earns its LLM
-
Generative dashboard
Replace the plain HTML view with json-render driven by Maestro's
dashboard.json. Wire real auth (WorkOS / magic link). both · customer-facing surface -
The factory
Stand up Executor + Reviewer so new specialists are built, not hand-written. Tools become CLI + Skill repos under
tenwhy-tools/. both · the supply side - Multi-tenancy + billing Per-customer provisioning, RLS, the hiring flow, and customer billing (native Stripe shines in Impl 2). both · product-ready
7 · Open verification
Not blockers for Phase 1, but needed before Impl 2 hardens.
| Item | Status |
|---|---|
| Daytona sandbox | verified <90ms boot · dedicated kernel/fs/net per sandbox · TypeScript SDK + REST · open-source · ephemeral + snapshot. Strong µVM analog; the TS SDK fits our Node world. |
| Stripe Projects catalog | needs auth Install stripe plugin install projects + stripe login, then stripe projects catalog to confirm exact service refs (Daytona, Neon, WorkOS, OpenRouter, Firecrawl, Inngest, Sentry). |
| Better Stack in catalog? | verify If present, it moves inside Projects and the Impl 2 observability asymmetry disappears. If not, it's wired manually (like the GitHub brain). |
| Stripe Projects paid-tier countries | verify Paid provider provisioning is limited to certain countries — confirm coverage before committing Impl 2 to production. |
| Sandbox egress enforcement | verify Confirm Sapiom and Daytona enforce the job's allow-listed endpoints at the network layer. This is the runtime half of the security boundary (design decision #32) — if egress isn't network-enforced, "no tool spends money wrongly" has no teeth. |
reminder
Stripe Projects is a provisioning + credential + billing layer, not a
runtime. It never runs Maestro or the agents — it just makes sure the right vendors are
provisioned and the right keys are in .env. The architecture in
the design is unchanged by it.