Every decision we've made about what to build, how to build it, what to defer, and why — laid out end to end. The first three phases make up our MVP: enough of the core service to start selling it. Everything after that waits for traction.
The plumbing that everything else sits on. No one uses anything from Phase 0 directly, but if we skip it we pay for it ten times later.
What a single customer can do end to end with their workspace. Sign up free, create workspaces, use OpenBerth through the dashboard, manage credentials, see clear status — all without a credit card.
Stripe does the heavy lifting. Checkout for upgrades, Customer Portal for everything else. Webhooks drive state changes via an idempotency table. Plan changes unify with trial expiry under one suspension flow.
Everything lifecycle-adjacent that a customer needs to hear about. Resend for delivery, React Email for templates, Queues for async dispatch. Idempotency on sends.
Multi-account workspaces, invite-by-email flow, role management UI. Schema already supports it via memberships — this phase exposes the UI.
Full account deletion cascade with a 14-day grace period. Billing ownership transfer between owner-role members, subject to receiving account's plan capacity.
The primitive works from Phase 1 — a customer can create an OpenBerth user and paste the key into the open-source CLI. This phase adds polish, guided setup, MCP discovery.
The hardening work that stops becoming optional past a few thousand users, plus the growth features driven by customer demand.
| Feature | P1 | P2 | P3 |
|---|---|---|---|
| Account & auth | |||
| Sign in with Google | ● | ○ | ○ |
| Account profile | ● | ○ | ○ |
| Sign out | ● | ○ | ○ |
| Trial & paid plans | |||
| 14-day free trial, no card | ● | ○ | ○ |
| Trial countdown in dashboard | ● | ○ | ○ |
| Pricing page | ○ | ● | ○ |
| Upgrade via Stripe Checkout | ○ | ● | ○ |
| Customer Portal | ○ | ● | ○ |
| Plan downgrade with choose-to-keep UX | ○ | ● | ○ |
| Workspace lifecycle | |||
| Create, list, rename, delete workspace | ● | ○ | ○ |
| Region picker (EU, US) plan-gated | ● | ○ | ○ |
| Four-state status UX | ● | ○ | ○ |
| Live status during transitions | ● | ○ | ○ |
| Suspend on trial / payment / downgrade | ● | ● | ○ |
| Grace period & automatic termination | ○ | ● | ○ |
| OpenBerth dashboard | |||
| Deployments: list, create, update, delete, logs | ● | ○ | ○ |
| Sandboxes: full CRUD + promote | ● | ○ | ○ |
| OpenBerth users: create, rotate, delete | ● | ○ | ○ |
| Secrets: per-user + global | ● | ○ | ○ |
| Workspace settings | ● | ○ | ○ |
| CLI access | |||
| OSS OpenBerth CLI compatibility | ● | ○ | ○ |
| Create named users with copyable keys | ● | ○ | ○ |
| Email notifications | |||
| Welcome on sign-up | ○ | ○ | ● |
| Trial ending / ended | ○ | ○ | ● |
| Payment events | ○ | ○ | ● |
| Workspace suspended / resumed / at risk | ○ | ○ | ● |
Four strict layers, each depending only on the ones below. Presentation is thin. Application orchestrates. Domain is pure logic. Infrastructure is adapters.
One DO instance per workspace. All state transitions for that workspace funnel through it. Concurrent refreshes, queue events, and cron-driven updates can't race because the DO serializes them by construction. This is the concurrency primitive the architecture depends on to scale past a few thousand customers without adding locks or careful transaction choreography.
Plans live in plans/catalog.ts, versioned with the app. Each plan ID includes a suffix (@1, @2). Once a version ships, it's immutable — grandfathered customers stay on it forever unless explicitly migrated. To change anything, create a new version.
Shown on pricing page. New signups can pick it. Billed normally.
Hidden from pricing. Existing customers keep their entitlements until explicit migration.
Force-migrate or downgrade at renewal. Last resort — costs customer trust.
The workspace state machine has eight internal states, but users only see four labels. A declarative mapping table translates raw infra states into our vocabulary. Unmapped infra states fall through to unknown — never silently broken.
Every stateful entity has a desired state and an observed state. Reconcilers close the gap. Never let a user action write the observed column — only reconciliation does.
Every state transition for a workspace flows through its Durable Object. No concurrent paths. No locks, no coordination bugs, no partial writes.
Infra states, Stripe events, OpenBerth errors — translate them all into our vocabulary. Their shapes can change. Ours doesn't.
Every Server Action, use case, and route handler calls the same authorize(account, permission, resource). Plan entitlements and invariants live there — nowhere else.
Versioned catalog in TypeScript. Immutable. Reviewed in PRs. Never edited in place. Grandfathering is not a feature — it's the default behavior.
Admin keys decrypted in-request only. No caching, no logging, no return-to-client. Master key from Secrets Store. Ciphertext in D1.
If a label uses jargon, it's wrong. If an error says provisioning, it's wrong. Engineering vocabulary stays out of the UI and out of emails.
Add schema capacity early because migrations are expensive. Don't add abstractions until you've hit the problem twice. Refactors are cheap; wrong abstractions aren't.
Outbox, audit log, plugin system, public API, custom roles — all considered, all deferred. They earn their place with specific pressure, not with speculative nice-to-have.
Every item here was considered during design and intentionally left out of the MVP. Each has a clear path back in when it becomes necessary — specific pressure, not speculative nice-to-have.
When infra.provision() succeeds but our DB write fails, we have an orphan. Reconciliation job with idempotency keys is the plan — exact contract with infra TBD.
Does the error enumerate which resources block deletion? If not, showing 3 deployments, 1 sandbox requires extra lookups.
Is there a server-wide sandbox list API, or only deployment-scoped? Affects the view this OpenBerth user's resources UX.
Do creator-owned global secrets block deletion? How is that surfaced to the account?
Immediate hard delete, or recoverable with a grace window? Pending decision — stronger case for grace at this scale.
Hard suspension (current plan) or read-only existing workspaces? Hard suspension creates upgrade pressure; read-only is kinder.