All posts
3 min read

Scaling SaaS from $0 to $1M MRR: A Technical Playbook

Building a SaaS product that reaches $1M MRR isn't just about writing good code. It's about making the right architectural decisions at the right time, knowing when to invest in infrastructure, and understanding which shortcuts will cost you later.

This is the playbook I used to scale a multi-tenant platform from zero to $800K MRR in 18 months.

Phase 1: The $0-10K Foundation

At this stage, speed matters more than everything else. Your architecture needs to support rapid iteration, not millions of users.

The Stack That Shipped Fast

// Keep it simple: monolith with clear boundaries
// Don't microservice at $0 MRR
const stack = {
  framework: "Next.js",
  database: "PostgreSQL",
  auth: "NextAuth.js",
  hosting: "Vercel + Supabase",
  payments: "Stripe",
};
Tip

The best architecture at $0 MRR is the one that lets you ship features to customers the fastest. A well-structured monolith beats a poorly designed microservices setup every time.

Database Design That Scales

Multi-tenancy needs to be right from day one. I went with schema-based isolation in PostgreSQL:

-- Tenant isolation via schemas
CREATE SCHEMA tenant_acme;
CREATE TABLE tenant_acme.users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT UNIQUE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);
 
-- Row-level security as a safety net
ALTER TABLE public.shared_resources ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON public.shared_resources
  USING (tenant_id = current_setting('app.tenant_id')::uuid);

Phase 2: The $10K-100K Growth Stage

This is where most technical decisions compound. The shortcuts you took in Phase 1 start showing cracks.

Caching Strategy

// Tiered caching: memory -> Redis -> database
async function getSubscription(tenantId: string) {
  // L1: In-memory (per-request)
  const cached = requestCache.get(`sub:${tenantId}`);
  if (cached) return cached;
 
  // L2: Redis (shared across instances)
  const redisCached = await redis.get(`sub:${tenantId}`);
  if (redisCached) {
    requestCache.set(`sub:${tenantId}`, redisCached);
    return JSON.parse(redisCached);
  }
 
  // L3: Database
  const sub = await db.subscription.findUnique({
    where: { tenantId },
  });
 
  await redis.setex(`sub:${tenantId}`, 300, JSON.stringify(sub));
  return sub;
}

Background Jobs

Anything that takes more than 200ms should be a background job. I moved to a queue-based architecture early:

  • Email sending
  • Webhook delivery
  • Report generation
  • Data aggregation
Warning

Don't wait until your API response times hit 5 seconds to add background processing. By then, you've already trained your users to expect slowness.

Phase 3: The $100K-1M Scale

At this point, you need observability, reliability, and performance engineering.

The Three Pillars

  1. Structured logging everywhere. Every request gets a correlation ID.
  2. Metrics on every database query, external API call, and queue operation.
  3. Distributed tracing across service boundaries.
// Every operation gets traced
const span = tracer.startSpan("process_payment", {
  attributes: {
    "tenant.id": tenantId,
    "payment.amount": amount,
    "payment.currency": currency,
  },
});

Database Scaling

PostgreSQL got us to $500K MRR with:

  • Connection pooling via PgBouncer
  • Read replicas for reporting queries
  • Partitioning on high-volume tables
  • Proper indexing strategy (no more sequential scans)

Key Takeaways

The biggest lesson: architecture should evolve with revenue, not ahead of it. Every dollar of MRR earned validates the next level of infrastructure investment.

Don't build for 1M users when you have 10. But do build foundations that won't require a complete rewrite at 10,000.