How to Host a SaaS Application: Multi-Tenant Architecture, Auth, Jobs, and Scaling

17 min read
saas hostingmulti-tenantcloud architecturebackground jobsscalingmonitoring

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.

ModelDescriptionIsolationCostComplexity

Shared database, shared schemaAll tenants in the same tables with a `tenant_id` columnLowLowestLow
Shared database, separate schemasEach tenant gets a PostgreSQL schemaMediumLowMedium
Separate databasesEach tenant gets their own database instanceHighestHighestHigh

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:

  • RLS is enforced at the database level -- application bugs cannot bypass it
  • `SET LOCAL` scopes the tenant setting to the current transaction
  • Performance impact is minimal (1-3% overhead on indexed tenant_id columns)
  • Every table with tenant data must have RLS enabled and a policy created
  • Always add an index on `tenant_id` for every table using 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.

    ComponentRecommendationWhy

    Password hashingbcrypt (12 rounds)Industry standard, adaptive cost
    Session tokensJWT with 24h expiryStateless, scalable across replicas
    Refresh tokensOpaque tokens in DBRevocable, long-lived (30 days)
    CSRF protectionSameSite cookiesNo extra tokens needed
    Rate limiting5 attempts / 15 min per IPPrevents brute force
    MFATOTP (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.

    FrameworkLanguageBest forQueue backend

    CeleryPythonComplex workflows, schedulesRedis, RabbitMQ
    Bull / BullMQNode.jsSimple queues, rate limitingRedis
    SidekiqRubyRails applicationsRedis
    TemporalAnyLong-running, durable workflowsSelf-hosted

    For a FastAPI or Node.js SaaS, the recommended setup is:

  • Redis as the job queue backend (available as a managed service on Arxentra)
  • Celery (Python) or BullMQ (Node.js) as the job framework
  • A separate worker service that processes jobs from the queue
  • <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:

  • Global rate limiting: Applied per IP address to prevent DDoS and brute force. Handled by Arxentra's built-in rate limiter or a reverse proxy.
  • Tenant rate limiting: Applied per API key or tenant to enforce plan limits. Implemented in your application.
  • PlanRate limitBurst limitDaily limit

    Free60 req/min10 req/sec1,000 req/day
    Starter300 req/min50 req/sec50,000 req/day
    Pro1,000 req/min200 req/secUnlimited
    EnterpriseCustomCustomUnlimited

    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

    ComponentConfigurationArxentra plan

    Web/API server1 replica, 1 vCPU, 1 GBStarter ($5/mo)
    PostgreSQL1 GB, single nodeStarter ($7/mo)
    Redis256 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

    ComponentConfigurationArxentra plan

    Web/API server2 replicas, 2 vCPU, 4 GBPro ($20/mo x 2)
    PostgreSQL4 GB, multi-AZPro ($30/mo)
    Redis1 GB (sessions + cache + jobs)Starter ($10/mo)
    Worker1 replica, 1 vCPU, 1 GBStarter ($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

    ComponentConfigurationArxentra plan

    Web/API server3-10 replicas, auto-scalingPro ($20/mo x avg 5)
    PostgreSQL8 GB, multi-AZ, read replicaBusiness ($60/mo)
    Redis4 GB (cluster mode)Pro ($25/mo)
    Worker2-4 replicasStarter ($5/mo x 3)
    CDNStatic assetsIncluded
    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?

    MetricTargetAlert 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.