Step 1: Pick a “boring” baseline stack
Your MVP is not the time to invent infrastructure. Choose tools with predictable ops: Postgres, a job queue, and a straightforward API layer.
Step 2: Define boundaries (even in one repo)
“Boundaries” doesn’t mean microservices. It means your business logic shouldn’t be scattered across React components and random API handlers. Keep a clean home for domain logic.
src/
app/ # Next.js routes + pages
components/ # UI components
lib/ # shared helpers
domain/ # your business rules (pure-ish logic)
billing/
orgs/
users/
server/ # DB, queues, integrations
db/
jobs/
integrations/The “domain” folder is the trick. It becomes the stable center of your app.
Step 3: Decide your tenant model early
Multi-tenancy touches everything: queries, permissions, billing, and even analytics. Pick one model now.
Shared schema (most common)
Every table includes tenant_id. Easiest to operate. Requires discipline in queries.
DB per tenant (heavier ops)
Stronger isolation. Harder migrations. Usually only worth it for strict enterprise cases.
Every tenant-owned table gets:
- tenant_id (indexed)
- created_at, updated_at
- created_by_user_id (optional)
And every query must filter by tenant_id.
If you forget tenant_id filters even once, you create data-leak risk and expensive future refactors.Step 4: Put RBAC in place early
The easiest RBAC is: org → membership → role. Keep it small and expand later.
orgs
id, name
users
id, email
memberships
id, org_id, user_id
role # 'owner' | 'admin' | 'member'Don’t scatter permission checks in UI components. Make your API reject unauthorized actions. The UI can hide buttons, but the API must enforce rules.
Step 5: Make migrations a first-class habit
If you skip migrations early, you’ll fear schema changes later. Every PR that changes schema must include a migration. Keep seed data separate, and test migrations on a fresh DB.
Step 6: Separate request work from background work
Your API should stay fast and predictable. Anything that can take seconds (PDF processing, emails, large exports) belongs in background jobs. The API enqueues the job and returns an ID; the worker does the heavy lifting.
Step 7: Add “basic observability”
You don’t need a fancy platform. You need the basics to ensure production remains calm.
- Error tracking (server + client)
- Request latency (p50/p95)
- Job failures and retries
- Auth events (logins, invites)
- Billing events (subscription changes, failed payments)
Step 8: Security defaults that pay off
Most SaaS breaches come from simple mistakes. These guardrails are cheap early, expensive late.
- Validate inputs at API boundary
- Rate limit logins & sensitive actions
- Signed URLs for uploads
- Secrets in env manager, never DB
- Audit role and billing changes
Step 9: Scaling without drama
Scaling is usually a sequence of small upgrades, not one big rewrite.
- → Add read replicas only after you know what queries are heavy
- → Cache expensive reads only after you measure bottlenecks
- → Split workers by job type when one queue starts blocking everything
- → Add an API gateway/rate limits before large integrations
- → Add per-tenant limits (max seats, max usage) to protect reliability
Frequently Asked Questions
Do I need microservices to “scale”?↓
Almost never at MVP stage. Clean boundaries inside one codebase gets you very far. Split services only when measurement proves it’s necessary.
What’s the biggest early architecture mistake?↓
Skipping tenant + RBAC decisions early. You can add features later, but retrofitting multi-tenancy and permissions touches everything.
When should I add billing?↓
As soon as pricing becomes real. Even before full Stripe flows, design your data model around plans, entitlements, and usage limits.