templates.SaaSStarter/SPEC.md

203 lines
13 KiB
Markdown
Raw Permalink Normal View History

2026-09-17 21:07:13 +00:00
# SaaS Starter — product specification
This is the contract the project is built against. It is written for the agent that scaffolds
the repository, but it doubles as the human-readable brief: every requirement below is meant to
be implementable and verifiable.
Values in angle brackets (`<Product name>`, `<billing provider>`, `<tenancy model>`,
`<auth provider>`) come from the project-creation dialog and are recorded in the project record;
replace them as you read.
## 1. Goal
A multi-tenant SaaS application a real product could launch with: sign up, create or join an
organisation/workspace, invite teammates, work inside a tenant-scoped dashboard, subscribe to a
plan with `<billing provider>`, watch usage against plan limits, and let a platform operator
support customers from a back-office admin area. Tenant isolation and server-side authorization
are the point of the exercise — this is not a set of screens wired together later.
It must be **runnable end-to-end on day one** (even with seeded demo data and the mock billing
provider), not a stub shell.
Non-goals (do not build unless the must-have list says otherwise): a full marketing CMS,
multi-region data residency, fine-grained per-object permissions beyond the roles below,
usage-based metering beyond a simple counter, white-labeling, a public API for third parties.
## 2. Roles
| Role | Can do |
|------|--------|
| Visitor | See the marketing/landing page, sign up, log in, accept an invitation. |
| Member | Everything inside their organisation's workspace: the dashboard, the product feature, their own profile. Read-only on members and billing. |
| Admin (owner/admin) | Everything a member can, plus manage the workspace: members and roles, invitations, plan and subscription, and workspace settings. |
| Billing admin | Manage the subscription, plan changes, payment method and invoices; cannot manage members or destructive workspace settings. |
| Platform operator | Cross-tenant back office: list and inspect organisations, subscriptions and audit events; impersonation is out of scope, support actions are auditable. |
Membership is per organisation and carries exactly one role. A user may belong to several
organisations and switches between them; all data reads and writes are scoped to the **active**
organisation. Authentication is required for everything except the landing page, signup, login
and invitation acceptance. `<auth provider>` decides the mechanism (`Email + OAuth`, magic link
or SSO/OIDC). Provide seeded accounts for local development.
## 3. Information architecture (routes)
Public:
- `/` — marketing/landing page: value proposition, features, pricing, call to action.
- `/pricing` — plan comparison and a path into signup.
- `/login`, `/signup`, `/logout`.
- `/invitations/:token` — accept an invitation; requires an account (sign up or log in first).
- `/forgot-password`, `/reset-password/:token`.
Workspace dashboard (guarded; scoped to the active organisation):
- `/app` — dashboard: usage against plan limits, recent activity, plan status, onboarding checklist.
- `/app/<feature>` — the first product feature from the must-have list, tenant-scoped.
- `/app/settings/workspace` — name, slug, workspace settings.
- `/app/settings/members` — list members, change roles, invite by email, revoke invitations.
- `/app/settings/billing` — current plan, seats, usage, plan change, portal link, invoices.
- `/app/settings/profile` — the signed-in user's name, email and preferences.
- `/app/organisations` — organisations the user belongs to; switch or create one.
Operator back office (guarded by the platform-operator role):
- `/admin` — dashboard: organisations, MRR/active subscriptions, recent signups, failed webhooks.
- `/admin/organisations` — list/search; `/admin/organisations/:orgId` — detail: members, plan,
subscription and usage.
- `/admin/subscriptions` — list with status and plan filters; `/admin/subscriptions/:id` — detail.
- `/admin/audit` — audit-event log with actor, action, target and tenant filters.
## 4. Data model
Minimum viable entities (add fields the requirements imply; keep them typed and validated).
Every tenant-owned table below carries `organisationId` and **every** query is scoped by it.
- **User** — id, email (unique), passwordHash (or external identity id), name, createdAt.
- **Organisation** — id, name, slug (unique), createdAt, createdByUserId.
- **Membership** — id, organisationId, userId, role (`owner` | `admin` | `member` |
`billing_admin`), status (`active` | `invited` | `removed`), createdAt. Unique on
(organisationId, userId).
- **Invitation** — id, organisationId, email, role, token (hashed), expiresAt, invitedByUserId,
acceptedAt?, status (`pending` | `accepted` | `revoked` | `expired`).
- **Plan** — id, key, name, priceMinor (integer minor units), currency, interval (`month` |
`year`), seatLimit, featureLimits (JSON: named counters, e.g. `projects`, `apiCalls`),
providerPriceId, isActive.
- **Subscription** — id, organisationId, planId, provider, providerSubscriptionId, status
(`trialing` | `active` | `past_due` | `canceled`), seats, currentPeriodStart,
currentPeriodEnd, cancelAtPeriodEnd, createdAt.
- **Invoice** — id, organisationId, subscriptionId, provider, providerInvoiceId, number,
amountMinor, currency, status (`draft` | `open` | `paid` | `void` | `uncollectible`),
periodStart, periodEnd, hostedUrl, createdAt.
- **UsageRecord** — id, organisationId, metric, quantity, periodStart, periodEnd, updatedAt.
Unique on (organisationId, metric, periodStart) so counters are upserted, not duplicated.
- **AuditEvent** — id, organisationId?, actorUserId?, actorType (`user` | `system` | `webhook`),
action, targetType, targetId, metadata (JSON), ip?, createdAt.
Money is stored in **integer minor units** and the currency from the plan; never use floats.
Every price is formatted with `Intl.NumberFormat`/`toLocaleString('en-US')` with the currency.
## 5. Key flows
1. **Sign up → workspace.** A visitor signs up with `<auth provider>`, becomes the `owner` of a
new organisation (created in one transaction with the owner membership), and lands on
`/app` with an onboarding checklist. Signing up without an organisation (`User accounts
only` tenancy) lands on an empty-state dashboard with a "create organisation" action.
2. **Invite → join.** An admin invites `teammate@example.com` as a `member`; an Invitation row
with a hashed token and an expiry is created and the invite email is logged (dev) or sent.
The teammate opens `/invitations/:token`, signs up or logs in, the invitation is accepted,
a Membership is created, and they land in the organisation's dashboard. An expired, already
accepted or revoked token shows a clear error and creates nothing.
3. **Subscribe → limits.** The owner opens `/app/settings/billing`, picks a plan, and is sent to
`<billing provider>` checkout (mock provider when `None (mock)` is selected). On success the
checkout returns to the app and the subscription becomes `active`; seats and feature limits
now come from the plan. Exceeding a feature limit is blocked **server-side** with a plan-
upgrade prompt, not just a disabled button.
4. **Webhook → state.** `<billing provider>` posts subscription/invoice events to
`/api/webhooks/billing`. The handler verifies the signature, deduplicates by the provider
event id, and updates Subscription / Invoice / plan limits idempotently — replaying the same
event must not apply it twice.
5. **Operator support.** A platform operator opens `/admin/organisations`, finds a tenant,
inspects its plan, subscription, usage and recent audit events, and sees the failed-webhook
queue. The access is itself recorded as an AuditEvent.
## 6. Functional requirements
- **Auth:** signup, login, logout, password reset, and (`Email + OAuth`) at least one OAuth
provider; session/CSRF handling is server-side; brute-force is rate-limited.
- **Tenancy:** create organisation, switch between organisations, invite/revoke/accept members,
change a role. Role changes and removals are authorized server-side; the last owner cannot be
demoted or removed.
- **Dashboard:** tenant-scoped metrics, plan status, usage against limits, and an onboarding
checklist; every screen has explicit loading, empty and error states.
- **Billing:** plan picker, checkout redirect/session, customer portal link, invoices list,
subscription status; the provider integration sits behind one interface with a mock
implementation used when `None (mock)` is selected, so the flow works without keys.
- **Plan limits:** a single server-side guard used by every feature that consumes a metered
resource; `UsageRecord` counters are upserted per period; over-limit requests return a typed
error the UI renders as an upgrade prompt.
- **Admin/back office:** list and search organisations, subscriptions and audit events; no
destructive action without confirmation; support access is audited.
- **Seed data:** at least two organisations (one on a paid plan, one on a trial or free plan),
an owner and a member per organisation, one pending invitation, one subscription with an
invoice, and usage records — so a reviewer can log in immediately and see a populated app.
## 7. Non-functional requirements
- **Tenant isolation (top requirement):** every tenant-owned row is scoped by `organisationId`;
scoping is enforced **server-side** on every query and mutation (a shared repository/query
layer or row-level policy, not per-controller discipline); an organisation can never read or
mutate another's data, and the isolation is proven by an automated test that fails on a cross-
tenant read.
- **Security:** authorize every mutation server-side (never only in the UI); hash passwords with
a maintained library; keep all provider/billing secrets and keys server-side only and never
ship them to the client; verify webhook signatures; validate and cap every input; rate-limit
auth and webhook endpoints.
- **Idempotency:** webhook processing is deduplicated by provider event id and checkout/order-
like mutations are idempotent — a replayed event or a double submit must not double-apply.
- **Accessibility:** semantic landmarks, labelled inputs, visible focus, keyboard-operable
navigation and dialogs, contrast at least AA.
- **Performance:** lazy-load route chunks; list endpoints are paginated, never unbounded.
- **Resilience:** every screen and every API call has explicit loading, empty and error states;
a billing-provider outage degrades to a clear message instead of a broken page.
- **Observability:** structured logs for subscription/plan changes, webhook results and operator
actions; a health endpoint; audit events for privileged actions.
## 8. Acceptance criteria (definition of done)
- [ ] Install, dev server, lint, typecheck, tests and production build all pass.
- [ ] Tenant isolation is proven by an automated test: an authenticated user of organisation A
cannot read or mutate any organisation B resource, and the test fails if scoping is removed.
- [ ] Billing webhooks verify the provider signature and are idempotent: the same event
delivered twice changes state once.
- [ ] Plan limits are enforced server-side: at the limit, the guarded mutation is rejected with a
typed error even when the UI is bypassed with a direct request.
- [ ] A seeded organisation + owner lets a reviewer log in immediately and see a populated,
tenant-scoped dashboard; `.env.example` documents every secret, and `None (mock)` billing
runs with no keys.
- [ ] Signup → organisation → invite → accept → role change works end-to-end; the last owner
cannot be demoted or removed.
- [ ] Billing flow works end-to-end with the mock provider: plan pick, checkout return,
subscription active, invoices listed, portal link present.
- [ ] Admin/back office is unreachable without the platform-operator role; operator actions are
audited.
- [ ] No billing/provider secret is present in client code or client-delivered config.
- [ ] README quickstart (install, run, test, env) is accurate; empty/loading/error states exist.
- [ ] CI runs install + lint + typecheck + tests + build.
## 9. Suggested build order
Follow this order and finish (and verify) a layer before starting the next:
1. **Scaffold** the chosen stack, install dependencies, get the dev server and the empty shell
running, and commit the skeleton.
2. **Scope**: turn the routes and the data model above into the real router and schema/migrations,
including the `organisationId` columns and the shared tenant-scoping layer.
3. **Auth + tenant scoping** (`<auth provider>`): signup, login, organisation create/switch,
membership and roles — plus the cross-tenant isolation test, before any dashboard work.
4. **Dashboard shell** scoped to the active organisation, with loading/empty/error states.
5. **Billing + plan limits** (`<billing provider>`, mock first, then real): checkout, portal,
invoices, webhook handling with signature verification and idempotency, and the server-side
limit guard.
6. **First product feature** from the must-have list, consuming the tenant scope and a metered
limit.
7. **Admin** back office (`/admin`): organisations, subscriptions, audit, failed webhooks.
8. **Quality**: tests for the flows above (tenant isolation, webhooks, limits), CI, README,
`.env.example`, accessibility pass.