SaaS Architecture Checklist: 50 Decisions to Make Before You Scale

#saas architecture checklist
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Founder & Lead Developer

Expert in software development and legacy code optimization

Most SaaS products do not fail because of one big architectural mistake. They get slow and expensive to change because of dozens of small decisions that nobody made on purpose. Two years later a migration that should take a week takes a quarter.

This SaaS architecture checklist collects the 50 decisions I see causing the most pain in code audits of products between seed and Series B. For each one there is a default I would pick for a typical B2B SaaS, and the conditions under which a different choice makes sense. You do not need to get all 50 right at launch. You do need to know which ones you skipped. The post on SaaS architecture mistakes that kill startups at Series A covers the failure modes; this one is the reference version.

Data model: 12 decisions

  1. Multi-tenancy isolation. Default: shared database, shared schema, a tenant_id column on every tenant-scoped table, enforced at the query layer. Move to schema-per-tenant or database-per-tenant only when a customer contract or a regulator demands physical separation. See multi-tenant SaaS architecture patterns for the trade-offs.

  2. Where tenant scoping is enforced. Default: in one place (a repository base class, a query builder middleware, or Postgres row-level security), never hand-written per query. If a developer can forget the WHERE tenant_id, one of them eventually will.

  3. Primary key type. Default: UUIDv7 or ULID, time-ordered so indexes stay compact. Auto-increment integers leak record counts in URLs, and random UUIDv4 fragments B-tree indexes on large tables.

  4. Soft delete strategy. Default: a nullable deleted_at timestamp plus a partial index on deleted_at IS NULL. Hard delete only where the law requires it (GDPR erasure), and do that through a dedicated job, not ad hoc SQL.

  5. Timestamp storage. Default: timestamptz in Postgres, everything stored in UTC, converted at the edge. Local times without an offset cause duplicate cron runs and off-by-one-day reports.

  6. Timezone of the tenant vs. timezone of the user. Default: store both. Reports are per tenant, notifications are per user, and deciding this late means retrofitting columns everywhere.

  7. Money. Default: integer minor units (cents) plus an ISO currency code. Never float.

  8. Audit log architecture. Default: an append-only audit_events table written from application code at the service layer, with actor, tenant, entity, action and a JSON diff. Database triggers hide business meaning. Enterprise buyers will ask for this before they sign.

  9. Schema migrations. Default: versioned migration files in the repo, applied automatically in the deploy pipeline, always backward compatible with the previous app version. Expand-then-contract for every rename.

  10. JSON columns. Default: allowed for tenant-specific settings and unstructured metadata, forbidden for anything you filter or join on. The moment you write WHERE data->>'status' = 'active' in a hot path, that field wants a real column.

  11. Read models. Default: none until a screen is measurably slow. Then a materialized view refreshed by a job, not a second database.

  12. Full-text search. Default: Postgres tsvector until you need faceting, typo tolerance or ranking across millions of documents. Elasticsearch or Meilisearch after that, fed by the same event stream as everything else. The PostgreSQL schema design for SaaS post goes deeper on decisions 3 through 12.

Authentication and authorization: 8 decisions

  1. Build or buy identity. Default: buy the identity layer (Auth0, Clerk, Keycloak, WorkOS) if enterprise SSO is on the roadmap within 18 months. Build only with a compliance reason to keep credentials in-house and someone who has done it before.

  2. Session storage. Default: server-side sessions in Redis with an opaque cookie for browser clients. JWTs for API clients and service-to-service calls. A JWT in a browser cookie gives you the revocation problem of tokens with the CSRF surface of cookies.

  3. Token lifetime and rotation. Default: 15-minute access tokens, refresh tokens that rotate on every use, and a family-level revoke when a reused refresh token is detected.

  4. Where permissions are checked. Default: at the service or use-case boundary, once, with the result cached for the request. Controller-level checks leak as soon as a CLI or queue consumer becomes a second entry point.

  5. Permission model. Default: roles per tenant (owner, admin, member, read-only) with a small set of explicit permissions behind them. Add resource-level ACLs only when a customer asks for "this user can see project A but not project B".

  6. API keys. Default: hashed at rest like passwords, prefixed so they are recognisable in logs and secret scanners, scoped to a tenant and a permission set, with a last_used_at column so you can find dead keys.

  7. Password hashing. Default: argon2id, or bcrypt with a cost of 12 if the runtime lacks argon2. Not configurable per tenant.

  8. Impersonation. Default: build a support impersonation flow early, log every use to the audit table, show a banner to the impersonating staff member, and time-limit it.

API design: 9 decisions

  1. Versioning. Default: no version in the URL. Add fields, never remove or rename them, and use a date-based header version only if you eventually need a breaking change.

  2. Pagination. Default: cursor-based with an opaque cursor and a next_cursor in the response. Offset pagination is fine for admin screens and falls apart on large tables where rows change between pages.

  3. Error response schema. Default: one shape for every error, everywhere: a machine-readable code, a human message, an optional field for validation errors, and a request_id. Pick RFC 9457 problem details if you want a standard to point at.

  4. Idempotency keys. Default: required on every non-GET endpoint that creates or charges something. Store the key with the tenant, the request hash and the response for 24 hours. Otherwise every retry from a flaky network is a duplicate order.

  5. Rate limiting. Default: per tenant and per API key, token bucket in Redis, with RateLimit-* headers in the response. Add per-endpoint limits for expensive operations like exports.

  6. Field selection and expansion. Default: none. Return a stable representation and make it fast. Add sparse fieldsets when a customer measures the difference.

  7. Webhooks out. Default: signed payloads (HMAC with a per-endpoint secret), at-least-once delivery with exponential backoff, a visible delivery log in the UI, and an event type in every payload.

  8. Bulk operations. Default: asynchronous. A bulk endpoint accepts the job, returns a job ID, and the client polls or receives a webhook. Synchronous bulk endpoints produce timeouts and half-applied changes.

  9. Public API vs. internal API. Default: the same API, with the internal frontend as its first customer. Two APIs drift within months.

Background jobs: 7 decisions

  1. Transport. Default: a proper queue (SQS, RabbitMQ, Redis Streams, or Symfony Messenger and BullMQ on top of one of them), never a database table polled every second. The database-as-queue pattern works until the first lock contention incident.

  2. Retry strategy. Default: exponential backoff with jitter, capped at five attempts. Fixed one-second retries turn a provider outage into a self-inflicted one.

  3. Dead letter handling. Default: every queue has a dead letter queue, and the dead letter queue has an alert. A job that fails five times is a bug report, not a statistic.

  4. Job idempotency. Default: every handler is safe to run twice. At-least-once delivery means the queue will run it twice, eventually.

  5. Payload contents. Default: IDs only, never serialized entities. The handler reloads fresh state. Serialized entities are stale by the time the job runs.

  6. Scheduling. Default: one scheduler process that enqueues jobs, and workers that execute them. Cron on the app server double-runs the day you add a second instance. The post on scheduled jobs and cron at scale has the details.

  7. Long-running work. Default: split anything that runs longer than a few minutes into a chain of smaller jobs with progress persisted between steps. A single three-hour job cannot be deployed over or retried without starting again.

Observability: 7 decisions

  1. Log format. Default: structured JSON with a fixed set of top-level fields (timestamp, level, message, request_id, tenant_id, user_id, service) from the first day.

  2. Trace context propagation. Default: OpenTelemetry, with the trace ID injected into every log line, every outgoing HTTP call and every queued job payload. Where you send the traces matters less than never having to retrofit the plumbing.

  3. Metric naming. Default: one convention, written down, following the OpenTelemetry semantic conventions where they exist.

  4. Error tracking. Default: Sentry or equivalent, with releases tagged and the tenant attached to every event.

  5. Health checks. Default: a shallow /health for the load balancer and a deep /ready that checks the database, the queue and the cache. Never wire the deep check to the load balancer, or one slow Redis call takes the fleet out of rotation.

  6. Per-tenant usage metrics. Default: count requests, storage and job executions per tenant from day one, even if you never bill on them. You will want it for pricing and for finding the tenant that is quietly costing you money.

  7. Alerting philosophy. Default: page on symptoms (error rate, latency, queue depth), not on causes (CPU, memory). Alerts that fire without someone acting are removed, not muted.

Deployment: 7 decisions

  1. Zero-downtime deploys. Default: rolling deploys behind a load balancer with connection draining, or blue-green if the platform makes it cheap. Both require old and new versions to run against the same database at once, which is decision 9 again.

  2. Migration timing. Default: migrations run as a separate step before the new app version starts, and they are written so that the old version keeps working while they run. Migrations on container startup race each other at two replicas.

  3. Rollback mechanism. Default: redeploying the previous image, which must be a one-command operation that someone has actually practised. Down migrations are not a rollback strategy: roll forward on data, roll back on code.

  4. Configuration and secrets. Default: environment variables injected by the platform for configuration, a secret manager (Vault, AWS Secrets Manager, Doppler) for secrets, and a startup check that fails fast if any required variable is missing.

  5. Environment parity. Default: one container image promoted through staging to production, with only configuration changing.

  6. Feature flags. Default: a flag system before you need the first flag, so that deploying and releasing are separate events. A homegrown table with a cache is enough to start. Delete flags older than three months.

  7. Data residency. Default: know which region every byte lives in, including backups, logs and third-party processors, and document it. European customers will ask, and "we are not sure" slows the deal down. When you actually need per-region isolation, that is an architecture project of its own, described in multi-region SaaS architecture for data residency.

How to use this SaaS architecture checklist

Do not treat the 50 defaults as rules. Treat them as the answers you have to argue against. Go through the list with your tech lead and mark each item as done, deliberately different, or undecided. The undecided column is your real technical debt register. It is usually longer than the team expects, and most of it is cheap to fix at 100 customers and expensive at 1,000.

A few of these decisions are close to irreversible once data exists: the tenancy model, primary key types, timestamp storage, and the audit log. Those deserve an hour of discussion before the first migration. Most of the rest can be changed later at a cost that stays roughly constant. The dangerous ones are those where the cost grows with the codebase, like where permissions are checked and how logs are structured.

If you are building a new product, Wolf-Tech helps teams make these calls early through tech stack strategy and custom software development engagements. If you already have a product and want to know how many of the 50 you got wrong, a code and architecture audit produces exactly this list with your answers filled in. Write to hello@wolf-tech.io or have a look at wolf-tech.io.