Building a SaaS application requires architectural decisions that are difficult and expensive to change later. This guide covers the infrastructure and architecture patterns that successful SaaS companies use: multi-tenant data isolation, authentication, background job processing, rate limiting, and scaling from 100 to 100,000 users.
Every recommendation is based on patterns used by SaaS companies processing millions of requests daily. We start with the simplest viable architecture and explain when and how to evolve it.
Multi-tenant database models
Q: What are the options for multi-tenant data isolation?
There are three models for storing data from multiple tenants (customers). Each has different tradeoffs for complexity, isolation, and cost.
| Model | Description | Isolation | Cost | Complexity |
| Shared database, shared schema | All tenants in the same tables with a `tenant_id` column | Low | Lowest | Low |
| Shared database, separate schemas | Each tenant gets a PostgreSQL schema | Medium | Low | Medium |
| Separate databases | Each tenant gets their own database instance | Highest | Highest | High |
For most SaaS applications, use shared database with shared schema and PostgreSQL Row-Level Security (RLS). This model supports thousands of tenants on a single database with strong isolation guarantees enforced at the database level. It is the model used by Stripe, Notion, and Linear.
Switch to separate schemas when tenants require regulatory data isolation (healthcare, finance). Switch to separate databases only when tenants require physical isolation for compliance or performance guarantees.
PostgreSQL Row-Level Security (RLS)
Q: How does RLS enforce tenant isolation?
RLS adds automatic WHERE clauses to every query, ensuring a tenant can only see their own data. Even if your application code has a bug that omits the `tenant_id` filter, the database enforces the boundary.
-- Enable RLS on a table
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Create a policy that restricts access to the current tenant
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.current_tenant_id')::int);
-- Force RLS even for table owners
ALTER TABLE projects FORCE ROW LEVEL SECURITY;
In your application, set the tenant context at the beginning of each request:
<h1>FastAPI middleware for tenant context</h1>
async def set_tenant_context(request, call_next):
tenant_id = get_tenant_from_token(request)
async with db.acquire() as conn:
await conn.execute(
"SET LOCAL app.current_tenant_id = $1",
str(tenant_id)
)
# All subsequent queries in this transaction
# are automatically filtered by tenant_id
return await call_next(request)
Key points about RLS:
Authentication architecture
Q: How should SaaS authentication be structured?
A production SaaS authentication system needs: user registration, login, session management, password reset, email verification, and multi-tenant access control.
| Component | Recommendation | Why |
| Password hashing | bcrypt (12 rounds) | Industry standard, adaptive cost |
| Session tokens | JWT with 24h expiry | Stateless, scalable across replicas |
| Refresh tokens | Opaque tokens in DB | Revocable, long-lived (30 days) |
| CSRF protection | SameSite cookies | No extra tokens needed |
| Rate limiting | 5 attempts / 15 min per IP | Prevents brute force |
| MFA | TOTP (Google Authenticator) | No SMS costs, offline-capable |
For most SaaS startups, building authentication from scratch is not recommended. Use an established library or service:
- Next.js: NextAuth.js (open source)
- FastAPI: FastAPI-Users or custom JWT implementation
- Managed service: Clerk, Auth0, or Supabase Auth
If you build custom auth, store sessions in Redis for fast lookup and easy invalidation across replicas. Never store plain-text passwords -- use bcrypt with a work factor of at least 12.
Background job processing
Q: How do I handle background jobs in a SaaS application?
Background jobs handle tasks that should not block the user request: sending emails, generating reports, processing uploads, syncing external data, and running scheduled maintenance.
| Framework | Language | Best for | Queue backend |
| Celery | Python | Complex workflows, schedules | Redis, RabbitMQ |
| Bull / BullMQ | Node.js | Simple queues, rate limiting | Redis |
| Sidekiq | Ruby | Rails applications | Redis |
| Temporal | Any | Long-running, durable workflows | Self-hosted |
For a FastAPI or Node.js SaaS, the recommended setup is:
<h1>tasks.py (Celery task definition)</h1>
from celery import Celery
import os
celery_app = Celery("tasks", broker=os.environ["REDIS_URL"])
@celery_app.task(bind=True, max_retries=3)
def send_welcome_email(self, user_id, tenant_id):
try:
user = get_user(user_id, tenant_id)
send_email(user.email, "Welcome to Our Platform", template="welcome")
except Exception as exc:
self.retry(exc=exc, countdown=60)
Deploy the worker as a separate service on Arxentra. In your `arxentra.yaml`:
services:
api:
build: ./Dockerfile
port: 8000
worker:
build: ./Dockerfile.worker
command: celery -A tasks worker --loglevel=info
databases:
postgres:
type: postgresql
plan: starter
redis:
type: redis
plan: starter
Rate limiting
Q: How should I implement rate limiting for a SaaS API?
Rate limiting protects your API from abuse, ensures fair usage across tenants, and prevents a single customer from consuming all resources. Implement rate limiting at two levels:
| Plan | Rate limit | Burst limit | Daily limit |
| Free | 60 req/min | 10 req/sec | 1,000 req/day |
| Starter | 300 req/min | 50 req/sec | 50,000 req/day |
| Pro | 1,000 req/min | 200 req/sec | Unlimited |
| Enterprise | Custom | Custom | Unlimited |
Use Redis for rate limit counters with sliding window algorithm:
<h1>rate_limiter.py</h1>
import time
import redis.asyncio as redis
class RateLimiter:
def __init__(self, redis_client):
self.redis = redis_client
async def check_rate_limit(self, tenant_id, limit_per_minute):
key = f"rate:{tenant_id}:{int(time.time()) // 60}"
count = await self.redis.incr(key)
if count == 1:
await self.redis.expire(key, 60)
return count <= limit_per_minute
Return proper rate limit headers in API responses:
- `X-RateLimit-Limit`: Maximum requests per window
- `X-RateLimit-Remaining`: Requests remaining in current window
- `X-RateLimit-Reset`: Unix timestamp when the window resets
- `Retry-After`: Seconds to wait (on 429 responses)
Scaling strategies
Q: How do I scale a SaaS application from 100 to 100,000 users?
Scaling a SaaS application follows a predictable progression. At each stage, specific bottlenecks emerge and specific solutions apply.
Stage 1: 100-1,000 users
| Component | Configuration | Arxentra plan |
| Web/API server | 1 replica, 1 vCPU, 1 GB | Starter ($5/mo) |
| PostgreSQL | 1 GB, single node | Starter ($7/mo) |
| Redis | 256 MB (sessions only) | Starter ($5/mo) |
| Total | $17/mo |
At this stage, a single server handles everything. Focus on product development, not infrastructure. The only scaling concern is database indexing -- add indexes on columns used in WHERE clauses and JOIN conditions.
Stage 2: 1,000-10,000 users
| Component | Configuration | Arxentra plan |
| Web/API server | 2 replicas, 2 vCPU, 4 GB | Pro ($20/mo x 2) |
| PostgreSQL | 4 GB, multi-AZ | Pro ($30/mo) |
| Redis | 1 GB (sessions + cache + jobs) | Starter ($10/mo) |
| Worker | 1 replica, 1 vCPU, 1 GB | Starter ($5/mo) |
| Total | $85/mo |
Key changes: add a second API replica for high availability, enable database failover (multi-AZ), introduce a background worker for async tasks, add application-level caching with Redis.
Stage 3: 10,000-100,000 users
| Component | Configuration | Arxentra plan |
| Web/API server | 3-10 replicas, auto-scaling | Pro ($20/mo x avg 5) |
| PostgreSQL | 8 GB, multi-AZ, read replica | Business ($60/mo) |
| Redis | 4 GB (cluster mode) | Pro ($25/mo) |
| Worker | 2-4 replicas | Starter ($5/mo x 3) |
| CDN | Static assets | Included |
| Total | $250-400/mo |
Key changes: enable horizontal auto-scaling, add a PostgreSQL read replica for read-heavy queries, upgrade Redis to cluster mode, add CDN for static assets, implement connection pooling (PgBouncer).
Monitoring and alerting
Q: What should I monitor in a SaaS application?
| Metric | Target | Alert threshold |
| API response time (p95) | < 200ms | > 500ms for 5 min |
| Error rate (5xx) | < 0.1% | > 1% for 5 min |
| Database query time (p95) | < 50ms | > 200ms for 5 min |
| CPU utilization | < 70% | > 85% for 10 min |
| Memory utilization | < 80% | > 90% for 5 min |
| Disk utilization | < 70% | > 85% |
| Background job queue depth | < 100 | > 1,000 for 10 min |
| Active database connections | < 80% of max | > 90% of max |
Arxentra provides built-in monitoring for CPU, memory, network, and disk metrics. For application-level metrics (response time, error rate, queue depth), integrate Sentry (error tracking), Datadog or Grafana Cloud (metrics), and PagerDuty or Opsgenie (alerting).
Frequently asked questions
Q: Should I use a monolith or microservices?
Start with a monolith. Every successful SaaS company started as a monolith and extracted services only when specific scalability bottlenecks emerged. A monolith is simpler to develop, deploy, debug, and monitor. Extract a service only when a component has fundamentally different scaling requirements (e.g., a video processing pipeline that needs GPU instances).
Q: How do I handle file uploads?
Never store uploaded files on the application server's local disk -- container restarts delete them. Use object storage (Arxentra Object Storage, AWS S3, or Cloudflare R2). Upload files directly from the browser using pre-signed URLs to avoid routing large files through your API server.
Q: How do I implement multi-tenant billing?
Use Stripe for billing. Stripe's subscription API handles plan management, proration, invoicing, and payment method management. Map each tenant to a Stripe customer, each plan to a Stripe product/price, and use Stripe webhooks to update tenant status in your database. This is significantly simpler and more reliable than building billing in-house.
Q: How do I handle tenant onboarding?
Automate tenant provisioning: when a user signs up, create the tenant record, assign default plan limits, provision any required resources (database schema, storage bucket), and send the welcome email -- all in a single background job. Target onboarding time: under 30 seconds from signup to usable dashboard.
Q: What about data backups?
Arxentra managed databases include automated daily backups with point-in-time recovery. For additional protection, schedule weekly pg_dump exports to object storage using a cron job. Test your restore process quarterly -- backups that have never been tested are not real backups.
Q: How do I handle database migrations in a multi-tenant setup?
With shared-schema multi-tenancy, migrations are standard ALTER TABLE statements applied once to the shared database. Use Alembic (Python), Prisma Migrate (Node.js), or Rails migrations. Always test migrations against a copy of production data before applying to production. For large tables (10M+ rows), use online schema migration tools like pg-osc or gh-ost to avoid locking.
Q: When should I introduce caching?
Introduce caching when database query latency becomes the bottleneck (typically at 5,000+ active users). Cache at two levels: (1) application-level Redis cache for frequently-read data (user profiles, feature flags, plan limits), and (2) HTTP-level caching with Cache-Control headers for API responses that do not change frequently. Never cache tenant-sensitive data without tenant-scoped cache keys.
For deployment specifics, see the FastAPI production guide or the Next.js deployment guide. For cost comparisons across platforms, see Best AWS Alternatives for Startups.