# 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 (``, ``, ``, ``) 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 ``, 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. `` 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/` — 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 ``, 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 `` 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.** `` 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** (``): 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** (``, 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.