templates.SaaSStarter/SPEC.md

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

  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.