13 KiB
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
- Sign up → workspace. A visitor signs up with
<auth provider>, becomes theownerof a new organisation (created in one transaction with the owner membership), and lands on/appwith an onboarding checklist. Signing up without an organisation (User accounts onlytenancy) lands on an empty-state dashboard with a "create organisation" action. - Invite → join. An admin invites
teammate@example.comas amember; 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. - Subscribe → limits. The owner opens
/app/settings/billing, picks a plan, and is sent to<billing provider>checkout (mock provider whenNone (mock)is selected). On success the checkout returns to the app and the subscription becomesactive; 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. - 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. - 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;
UsageRecordcounters 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.exampledocuments every secret, andNone (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:
- Scaffold the chosen stack, install dependencies, get the dev server and the empty shell running, and commit the skeleton.
- Scope: turn the routes and the data model above into the real router and schema/migrations,
including the
organisationIdcolumns and the shared tenant-scoping layer. - Auth + tenant scoping (
<auth provider>): signup, login, organisation create/switch, membership and roles — plus the cross-tenant isolation test, before any dashboard work. - Dashboard shell scoped to the active organisation, with loading/empty/error states.
- 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. - First product feature from the must-have list, consuming the tenant scope and a metered limit.
- Admin back office (
/admin): organisations, subscriptions, audit, failed webhooks. - Quality: tests for the flows above (tenant isolation, webhooks, limits), CI, README,
.env.example, accessibility pass.