A SaaS app is not a single-user dashboard with a checkout button. It needs a tenant or workspace boundary, members and roles, invitations and sessions, product records, plan rules, local access state, operator controls, migrations, privacy, export, and recovery. Billing and email providers sit outside that core and can fail independently.
This guide builds the smallest complete version first: two fictional customer workspaces, one product record per workspace, one invitation lifecycle, server-side authorization, one trial or plan-access policy, and a production test. It then shows where signed provider events, partial failures, cancellation grace, and tenant-safe migrations belong.

QUICK ANSWER
How do you build a SaaS app?
Define one tenant-scoped product record and lifecycle, then model workspaces, members, roles, invitations, sessions, plans, local access, and provider billing as separate records. Enforce tenant scope on every server route, test across two customer accounts, publish over HTTPS, and add signed provider events only after duplicate, out-of-order, and partial-failure handling is explicit.
Prepare two fictional customers before building screens
Two customer accounts are the minimum useful fixture. Tenant leaks, role mistakes, global exports, stale sessions, and plan-access bugs stay invisible when every test runs as the creator or one all-powerful administrator.
- One tenant-scoped product record and lifecycle: Name the first record customers create, its stable ID, tenant scope, owner, fields, status transitions, timestamps, version, deletion rule, and export shape. Bound the first release around one complete customer job.
- A workspace, membership, role, invitation, and session matrix: Write who may list, read, create, update, act on, delete, and export each record. Include invitation issue, acceptance, expiry, resend, and revocation plus role change, member removal, session expiry, and signed-out behavior.
- A plan and product-access policy: Define trial start and end, allowed trial actions, paid-through access, cancellation effective time, grace, suspension, reactivation, and operator override. Keep these local access decisions separate from provider customer, subscription, invoice, and event state.
- A migration, privacy, retention, export, and recovery owner: Name who approves customer-data fields, retention and deletion, authorized exports, schema backfills, failed tenant batches, record repair, and whole-app recovery before real customer data exists.
Credentials and access
| Credential | Minimum access | Storage and rotation |
|---|---|---|
| Optional payment-provider server credential Restricted server-side secret or API key issued by the payment provider you choose | Only the customer, checkout, subscription, invoice, refund, and read operations the documented flow actually needs; avoid account administration by default | Server-side secret configuration separated by environment, never browser code, source control, generated UI, URLs, analytics, or routine logs Create a replacement, update the server environment, verify one redacted test-mode request, monitor failures, then revoke the previous credential |
| Optional webhook signing secret Endpoint-specific signing secret from the selected provider | Verify only the exact development or production endpoint and event types registered for this application | Server-side secret configuration; verify against the raw request body and never print the secret or full payment payload Use the provider overlap or replacement procedure, verify old and new signatures during the allowed window, then remove the previous secret |
| Optional transactional-email credential Email API key or SMTP/API credential from the provider you select | Send only from the verified identity and environment required for invitation, trial, and access notifications | Server-side secret configuration, separated by environment and never returned to the browser Issue a replacement, send one fictional notification, verify delivery and error monitoring, then revoke the old credential |
Choose the tenant-isolation model before writing queries
Every private record needs one authoritative tenant scope, and every request needs a server-derived allowed scope. The storage model changes operational cost and blast radius, but no model removes the need to test identity, membership, action permission, exports, migrations, and recovery. When that operating model is ready to become a product brief, use the build a SaaS app builder path for the commercial implementation workflow. This article remains the informational field guide for designing and verifying the tenant, access, billing, migration, and recovery boundaries.
| Approach | Best for | Tradeoff |
|---|---|---|
| Shared database with a tenant key | A small first release where customer records share one schema and the team needs simple provisioning, queries, migrations, and backups. | It is efficient to operate, but every private query, cache key, job, file path, search index, and export must carry the correct tenant scope. |
| Separate schema per tenant | A product that needs stronger logical separation while retaining one database service and mostly shared application code. | It reduces some accidental cross-tenant queries but multiplies schema provisioning, migration ordering, drift checks, and support tooling. |
| Separate database per tenant | A concrete contractual, regulatory, regional, scale, or large-customer isolation requirement that justifies dedicated operations. | It improves physical separation but increases connection management, migrations, monitoring, backup, restore, incident response, and cost. |
Recommended:Start with a shared database and mandatory tenant keys for a small general SaaS first release. Centralize server authorization, scope every private query and cache, and run cross-tenant tests. Move to schema or database isolation only when a real requirement outweighs the operational cost.
Build a SaaS app step by step
Define tenant-scoped records, build membership and access, protect every server route, separate billing from product state, test cross-tenant and provider failures, migrate safely, publish, and operate the app.
STEP 01
Write one complete customer and operator journey
Bound the first release around one tenant-scoped record from invitation to export or deletion.
Describe workspace creation, invitation issue and acceptance, sign-in, first record creation, list, detail, update, one meaningful action, role change, trial or plan limit, session expiry, cancellation, export, and member removal in order.
For each screen and server action, name loading, empty, denied, stale, conflict, save-error, retry, and recovery behavior. Include the same known record ID in customer A, customer B, and a private session.
Expected result: The brief has one observable outcome for a visitor, invited person, authorized member, wrong customer, downgraded member, operator, expired session, limited plan, and failed save.
Verify it: Walk both fictional customers through the same product-record URL and action. If the allowed server result is ambiguous for either account, update the matrix before building.
STEP 02
Model tenant, membership, product, access, and provider records separately
Make each lifecycle explainable instead of collapsing the product into one paid-user flag.
Create separate tenant or workspace, user, membership, role or permission, invitation, session, product record, plan, product access, provider customer, subscription, invoice, provider event, and audit records as needed by the first release.
Give each stable IDs, authoritative tenant scope, lifecycle status, timestamps, and a version where concurrent updates matter. A provider customer or invoice belongs to the billing boundary; it is not the record that authorizes a product request.
Expected result: A role, invitation, session, plan, product access, subscription, invoice, and webhook event can each change without silently rewriting unrelated identity or tenant state.
Verify it: Walk two fictional workspaces through trial, paid, grace, canceled, suspended, reactivated, role downgrade, member removal, and invoice failure states and list the exact records changed by each transition.
STEP 03
Derive tenant and action scope on the server
Treat browser tenant IDs and hidden controls as presentation, never authorization.
Resolve identity from the server session, then derive active workspace membership, role, product access, and requested action from server records. Include the authorized tenant scope in every list, detail, update, action, file, background job, search, cache, and export path.
Do not query a private record globally and check its tenant afterward. Make tenant scope part of the lookup, and return a denial without private fields when no authorized record matches.
Expected result: Customer B cannot learn whether customer A has a known record ID, and signed-out, removed, downgraded, or suspended accounts cannot read or change it.
Verify it: Replay customer A list, detail, update, action, file, and export requests as customer B and in a private session, then inspect raw responses, cache keys, and background-job payload scope.
STEP 04
Build retry-safe invitations, roles, sessions, and product access
Make every long-lived grant revocable and every repeated technical attempt deterministic.
Issue a time-limited single-purpose invitation bound to the intended tenant, role, expiry, and one invitation record. Store only the safe server-side representation needed to verify it, and make acceptance return the existing successful result on a safe retry.
Define when role downgrade, member removal, trial end, grace, cancellation, and suspension take effect in existing sessions. Re-check current access server-side or document a short bounded authorization-cache window.
Expected result: One invitation creates one intended membership, and role, session, and access changes take effect according to the documented policy without duplicating grants.
Verify it: Accept the same invitation twice, interrupt one response, downgrade the member, expire the session, revoke access, and repeat the same protected request after each change.
STEP 05
Add provider billing behind signed, idempotent reconciliation
Treat payment state and product access as two operations with a partial-failure path.
When billing is needed, configure the chosen provider account, plan, test and production environments, restricted server credential, webhook endpoint, signing secret, and exact event types. Verify the signature against the raw server request before trusting the event.
Store every stable provider event ID once. Process retries idempotently, define precedence for out-of-order subscription and invoice events, and record pending, applied, ignored, and failed local transitions. Never grant access solely because a browser returned from checkout.
Expected result: Provider customer, subscription, invoice, and event history remain reviewable while local product access follows an explicit transition that can be retried or reconciled independently.
Verify it: With redacted test fixtures, replay one event ID, deliver an older event after a newer one, and simulate provider success followed by a failed access update and access success followed by a provider failure.
STEP 06
Make trials, cancellations, grace, and partial failures explicit
Choose the customer policy before provider event names accidentally choose it for you.
Store trial start, trial end, paid-through time, cancellation request, cancellation effective time, grace end, suspension reason, reactivation, and operator override only when the product needs them. Name the allowed actions in each local access state.
For payment succeeded but access failed, preserve the verified event and show reconciliation pending. For access granted but notification failed, keep access authoritative and retry only notification. For refund or chargeback states, define the human and provider review boundary instead of guessing.
Expected result: Customers and operators can explain current access and the next transition without treating an invoice label as the only source of product truth.
Verify it: Move a fictional tenant through trial end, paid-through cancellation, grace, failed invoice, reactivation, refund fixture, and notification failure while checking access, provider, and delivery records separately.
STEP 07
Test cross-tenant access, retries, ordering, sessions, and privacy
Exercise cases that the creator account and happy checkout cannot reveal.
Test signed out, private session, invited but unaccepted, customer A member, customer B member, operator, downgraded member, removed member, expired session, trial, grace, limited, suspended, and canceled states. Repeat direct requests, not only visible menu clicks.
Test invitation reuse, duplicate product create, stale version update, provider-event replay, out-of-order events, provider-access partial failure, notification failure after durable state, rate limits, log redaction, and an export request from the wrong tenant.
Expected result: Private data stays isolated, technical retries stay single, stale writes are detected, provider events reconcile deterministically, and routine logs contain bounded IDs rather than secrets or customer payloads.
Verify it: Record tenant, membership, product, event, access, and delivery counts before and after every case, then inspect raw responses and final records rather than trusting the visible success message.
STEP 08
Publish and run a two-tenant production smoke test
Verify the final HTTPS environment before inviting real customers or enabling live billing events.
Publish the app, create two clearly labeled fictional workspaces, invite one ordinary member to each, create and update one product record per tenant, change one role, exercise one plan-access boundary, and verify the protected operator route.
Check the final domain, HTTPS, cookies, environment separation, webhook endpoint configuration when applicable, denial responses, errors, logs, export, and cleanup of fictional records. Keep test and live provider credentials separated.
Expected result: Both tenants complete the intended workflow once, neither can access the other tenant, and role, session, plan, export, and operator boundaries match the documented policy in the final environment.
Verify it: Save bounded request, tenant, member, record, and event IDs, repeat customer A requests as customer B and signed out, then delete or archive the fictional data under the retention policy.
STEP 09
Migrate, monitor, export, and recover per tenant
Treat customer data evolution as a product operation, not a one-time schema command.
Apply schema changes through expand, backfill, verify, and contract. Keep old and new readers compatible during cutover, migrate in bounded tenant batches, record counts and rejected rows, verify application reads per tenant, and rehearse rollback or forward repair.
Monitor sign-in, invitations, denied actions, access changes, provider events, migration batches, exports, and operator corrections with stable bounded IDs. Keep code export, authorized tenant-data export, record repair, and whole-app restore as separate recovery decisions.
Expected result: An operator can diagnose and repair a tenant-specific access, provider, or migration problem without exposing another customer or restoring the whole application unnecessarily.
Verify it: Rehearse one failed tenant backfill, one stale role session, one provider reconciliation, one authorized tenant export, and one record repair, then document the exact bounded identifiers and recovery action used.
Minimum SaaS test matrix
Run these with stable fictional tenants and inspect stored records after every action. A correct-looking screen can still hide a global query, duplicate write, stale role, or misordered provider event.
| Test | Scenario | Expected result |
|---|---|---|
| happy path | Create two tenants, invite one member to each, complete one product workflow, change one role, and exercise one trial or plan-access transition. | Each tenant sees only its own records, memberships and access match the matrix, and every transition creates one intended result. |
| invalid input | Request customer A list, detail, update, action, and export routes signed out and as customer B; use an expired invite and a forbidden role action. | Every server request is denied or validated without leaking private fields or changing tenant, membership, product, provider, or access records. |
| retry | Repeat invitation acceptance, one idempotent product create, one provider event ID, an older event after a newer one, and a notification after durable access already changed. | Membership, product, event, access, and delivery state remain single, ordered by the documented rule, and independently recoverable. |
| production smoke | Complete the two-tenant workflow, role change, plan boundary, denial check, export check, and operator reconciliation on the published HTTPS app. | The final environment preserves tenant isolation, current access changes take effect under the session policy, and bounded logs identify every test without private payloads. |
SaaS failures and exact checks
Inspect the tenant, membership, session, product, plan, provider, access, and migration records before changing UI copy or adding a broad retry.
| Symptom | Likely cause | Check | Fix |
|---|---|---|---|
| One customer can retrieve another customer's record | A private query, cache, job, file path, search index, or export trusted a browser tenant ID or omitted the server-derived tenant scope. | Replay the known record ID as the other ordinary customer and inspect the server query scope, cache key, job payload, and response body. | Require the server-derived tenant scope in every private boundary, centralize authorization, and add cross-tenant regression fixtures for each route type. |
| A role downgrade or member removal works only after refresh | Role or membership authorization lives in browser state or a long-lived session claim without a current server check or invalidation rule. | Repeat the same protected server action from the existing session immediately after the change. | Re-check current membership and access on protected actions and define session invalidation or a short bounded authorization-cache window. |
| Payment succeeded but product access did not change | The signed provider event failed, arrived out of order, or persisted successfully while the separate local access transition failed. | Compare provider event ID, signature-verification result, processing status, subscription or invoice state, and local access transition without logging secrets or full payloads. | Persist verified events once, process idempotently, define ordering, and retry only the failed local transition through an operator reconciliation action. |
| A duplicate or older provider event reverses current access | The handler does not store stable event IDs or compare event meaning, effective time, and current local transition history. | Replay the event sequence from redacted fixtures and compare stored event IDs, timestamps, transition versions, and final access state. | Deduplicate by stable event ID, define event precedence and effective-time rules, and ignore or quarantine stale events for review. |
| A migration works for new tenants but breaks older data | The old schema was contracted before backfill and per-tenant verification completed, or historical tenant states were assumed uniform. | Compare old and new field counts, nulls, rejected rows, record versions, and application reads by tenant batch. | Restore compatible readers, return to expand/backfill/verify/contract, repair failed tenant batches, and remove the old path only after verified cutover. |
| A tenant export includes another customer or omits required records | The export queries globally, follows an unscoped relationship, or has no versioned export schema and completion check. | Trace every exported row and file to the authorized tenant and compare expected record counts and relationship edges with the source data. | Authorize export server-side, scope every query and storage reference, version the schema, and verify counts before releasing the file. |
Deploy and operate the tenant lifecycle
Deploy
Publish over HTTPS and verify the final domain, cookies, two-tenant journey, cross-tenant denials, role and session changes, plan boundary, operator controls, export, and cleanup with fictional accounts.
If billing or email is added, configure separate development and production accounts or credentials, register the exact production webhook endpoint and event types, verify signatures, and keep test events out of live customer state.
Ship schema changes with expand, backfill, bounded per-tenant verification, and contract. Keep old and new application versions compatible until the cutover is proven.
Monitor
Track sign-in and invitation failures, denied actions by reason, role and access transitions, provider-event processing, migration batches, export jobs, and operator corrections using request, tenant, member, record, event, and batch IDs.
Do not log passwords, sessions, invitation tokens, API keys, signing secrets, payment payloads, private product content, or whole customer exports. Bound error context and redact provider responses.
Recover
Support replacing an invitation, revoking sessions, correcting membership or access with an audit note, reconciling one verified provider event, retrying only failed notification, and repairing one tenant or migration batch.
Document source-code export, authorized tenant-record export, provider data, record repair, migration rollback or forward repair, and whole-app restore separately. A whole-app restore may return every tenant to older state, so compare narrower recovery first.
SaaS security and privacy checklist
Tenant isolation and authorization belong on the server. Minimize customer data, make every long-lived grant revocable, and treat providers, exports, logs, and migrations as separate boundaries.
- Derive identity, tenant membership, role, and current product access from server state for every private list, detail, update, action, file, job, search, cache, and export path.
- Use time-limited, single-purpose, revocable invitations and safe server-side token verification. Never place raw tokens in logs, analytics, screenshots, or generated assets.
- Keep provider credentials and signing secrets in server configuration by environment. Verify webhook signatures against the raw request, store each stable event ID once, and define out-of-order handling.
- Separate technical retry identity from record versions and human duplicate matching. One idempotency key cannot resolve stale concurrent edits or business-level duplicates.
- Collect only customer and member fields the product needs, bound input lengths, name retention and deletion owners, authorize exports, and keep private content out of routine logs.
- Test with two ordinary tenants, signed out, a private session, changed roles, removed members, expired sessions, plan changes, provider partial failures, and migrated historical records before real customers.
Questions about building a SaaS app
What is the minimum viable SaaS architecture?
One tenant or workspace record, user memberships, explicit roles, invitations and sessions, one tenant-scoped product record and lifecycle, local plan access, server-side authorization, two-customer tests, operator recovery, monitoring, export, and a production smoke test. Billing can be added after that boundary works.
Should every database record include a tenant ID?
Every private record needs an authoritative tenant path, whether direct or inherited through a rigorously checked parent relationship. Direct tenant keys are easier to query and audit for a small release. Shared global reference data should be explicitly classified rather than left unscoped by accident.
Can I use one database for all customers?
Yes, a shared database with mandatory tenant scope is a practical starting point for many small SaaS apps. The tradeoff is that every query, cache, job, file path, search index, and export must preserve tenant scope and be tested across two ordinary customer accounts.
Should billing status control authorization directly?
No. Keep provider customer, subscription, invoice, and event state separate from local plan and product-access state. Verified provider events can trigger explicit local transitions, but protected requests should authorize from a local state that operators can inspect, retry, and reconcile.
How do I handle payment succeeded but access failed?
Persist the verified provider event once and mark the local access transition failed or pending. Give an operator a reconciliation action that retries only that transition. Do not ask the provider to charge again and do not discard the successful payment evidence.
When should cancellation remove access?
Choose the policy explicitly: immediately, at the paid-through date, or after a defined grace period. Store the effective local access end time and test existing sessions, retries, failed invoices, reactivation, and provider event ordering against the same rule.
How should I deploy SaaS database migrations?
Use expand, backfill, verify, and contract. Keep old and new readers compatible, migrate in bounded tenant batches, record rejected rows, verify counts and application reads per tenant, and rehearse rollback or forward repair before removing the old schema path.
Does this guide prove a live billing integration?
No. It is a docs-backed implementation guide, not a tested Stripe or other provider report. A real billing path requires the selected provider account and plan, restricted server credentials, signed webhooks, redacted event fixtures, partial-failure tests, operational reconciliation, and final-environment verification.
Build the product boundary first
Turn one customer workflow into a tenant-safe first release
Describe the tenant, product record, member roles, access rules, tests, and operator path. Playcode can build the frontend, server logic, database, and hosted application around that bounded brief.
Build an App with PlaycodeProvider accounts, credentials, signed webhooks, policy review, and live billing verification remain separate when you add them.