DEV Community

Why There's No Example Code for B2B SaaS Apps (And What to Do About It)

If you've ever searched for "B2B SaaS starter code" or "complete SaaS application example," you've probably come up empty-handed. While GitHub overflows with todo apps, blog platforms, and e-commerce demos, comprehensive examples of actual B2B SaaS applications are frustratingly rare.

This isn't an accident. There are legitimate reasons why battle-tested B2B SaaS code remains locked away in private repositories. But more importantly, you don't need a complete example-you need the right patterns. Let's break down why examples are scarce and, more usefully, what architectural patterns you should implement instead.

Why B2B SaaS Examples Are Rare

Competitive Advantage: Unlike a todo app, a well-architected B2B SaaS application represents thousands of hours of refined engineering decisions. Companies rightfully view their codebase as intellectual property. The multi-tenancy strategy, billing integration, and permission system that took months to perfect aren't going to be open-sourced.

Complexity: B2B SaaS applications are complex beasts. They involve authentication, authorization at multiple levels, billing, subscription management, team management, audit logs, webhooks, API rate limiting, and data isolation. A truly representative example would be tens of thousands of lines of code-too large to be useful as a learning resource.

Context-Dependency: What works for a project management tool differs drastically from what works for a CRM or analytics platform. There's no one-size-fits-all B2B SaaS architecture, which makes creating a "canonical" example nearly impossible.

Liability Concerns: Publishing production-grade code for handling payments, user data, and multi-tenant isolation opens companies to scrutiny. If someone finds a security vulnerability in your open-source example, it reflects poorly on your brand.

The Critical Patterns You Actually Need

Instead of waiting for a mythical complete example, focus on implementing these core patterns correctly. Master these, and you'll be 80% of the way there.

1. Multi-Tenancy with Row-Level Security

The foundation of any B2B SaaS is proper data isolation. Here's a battle-tested PostgreSQL approach using row-level security:

// schema.prisma (using Prisma)
model Organization {
  id        String    @id @default(cuid())
  name      String
  slug      String    @unique
  users     User[]
  projects  Project[]
  createdAt DateTime  @default(now())
}

model Project {
  id             String       @id @default(cuid())
  name           String
  organizationId String
  organization   Organization @relation(fields: [organizationId], references: [id])
  createdBy      String
  @@index([organizationId])
}

// Middleware to enforce tenant isolation
import { PrismaClient } from '@prisma/client'

export function createTenantClient(organizationId: string) {
  const prisma = new PrismaClient()
  return prisma.$extends({
    query: {
      $allModels: {
        async $allOperations({ args, query, model }) {
          // Skip for Organization model
          if (model === 'Organization') return query(args)

          // Inject organizationId filter for all operations
          if (args.where) {
            args.where = { ...args.where, organizationId }
          } else {
            args.where = { organizationId }
          }

          // Inject organizationId for creates
          if (args.data && model !== 'User') {
            args.data = { ...args.data, organizationId }
          }

          return query(args)
        }
      }
    }
  })
}

This pattern ensures you can never accidentally query across tenant boundaries. It's saved me from data leakage bugs more times than I can count.

2. Role-Based Access Control (RBAC) That Scales

Skip the simple isAdmin boolean. You'll regret it. Here's a flexible RBAC system:

# models.py (using SQLAlchemy)
from enum import Enum
from sqlalchemy import Table, Column, String, ForeignKey
from sqlalchemy.orm import relationship

class Permission(Enum):
    PROJECT_CREATE = "project:create"
    PROJECT_READ = "project:read"
    PROJECT_UPDATE = "project:update"
    PROJECT_DELETE = "project:delete"
    BILLING_MANAGE = "billing:manage"
    USERS_INVITE = "users:invite"

user_roles = Table('user_roles', Base.metadata,
    Column('user_id', String, ForeignKey('users.id')),
    Column('role_id', String, ForeignKey('roles.id'))
)

role_permissions = Table('role_permissions', Base.metadata,
    Column('role_id', String, ForeignKey('roles.id')),
    Column('permission', String)
)

class Role(Base):
    __tablename__ = 'roles'
    id = Column(String, primary_key=True)
    name = Column(String, nullable=False)
    organization_id = Column(String, ForeignKey('organizations.id'))
    permissions = Column(ARRAY(String))  # Store as array for simplicity

class User(Base):
    __tablename__ = 'users'
    id = Column(String, primary_key=True)
    email = Column(String, unique=True, nullable=False)
    organization_id = Column(String, ForeignKey('organizations.id'))
    roles = relationship('Role', secondary=user_roles)

Authorization decorator:

from functools import wraps
from flask import g, abort

def require_permission(permission: Permission):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            user = g.current_user
            user_permissions = set()
            for role in user.roles:
                user_permissions.update(role.permissions)
            if permission.value not in user_permissions:
                abort(403, "Insufficient permissions")
            return f(*args, **kwargs)
        return decorated_function
    return decorator

Usage:

@app.route('/api/projects', methods=['POST'])
@require_permission(Permission.PROJECT_CREATE)
def create_project():
    # Your logic here
    pass

3. Subscription and Usage Tracking

You need to track usage limits and gate features based on subscription tiers:

// subscriptionService.ts
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export enum PlanTier {
  FREE = 'free',
  PRO = 'pro',
  ENTERPRISE = 'enterprise'
}

interface PlanLimits {
  maxProjects: number
  maxUsers: number
  apiCallsPerMonth: number
}

const PLAN_LIMITS: Record<PlanTier, PlanLimits> = {
  [PlanTier.FREE]: {
    maxProjects: 3,
    maxUsers: 5,
    apiCallsPerMonth: 1000
  },
  [PlanTier.PRO]: {
    maxProjects: 50,
    maxUsers: 50,
    apiCallsPerMonth: 100000
  },
  [PlanTier.ENTERPRISE]: {
    maxProjects: -1,
    maxUsers: -1,
    apiCallsPerMonth: -1
  }
}

export class SubscriptionService {
  async canPerformAction(
    organizationId: string,
    action: keyof PlanLimits
  ): Promise<boolean> {
    const org = await prisma.organization.findUnique({
      where: { id: organizationId },
      include: { subscription: true }
    })

    if (!org?.subscription) return false

    const limits = PLAN_LIMITS[org.subscription.tier as PlanTier]
    const limit = limits[action]

    // -1 means unlimited
    if (limit === -1) return true

    const currentUsage = await this.getCurrentUsage(organizationId, action)
    return currentUsage < limit
  }

  private async getCurrentUsage(
    organizationId: string,
    metric: keyof PlanLimits
  ): Promise<number> {
    switch (metric) {
      case 'maxProjects':
        return await prisma.project.count({ where: { organizationId } })
      case 'maxUsers':
        return await prisma.user.count({ where: { organizationId } })
      case 'apiCallsPerMonth':
        return await this.getAPICallsThisMonth(organizationId)
      default:
        return 0
    }
  }
}

Building Your Own Reference Architecture

Rather than searching for a complete example, build your own reference implementation incrementally:

  • Start with authentication: Use Auth0, Clerk, or Supabase Auth. Don't roll your own.
  • Add multi-tenancy: Implement the row-level security pattern above.
  • Implement RBAC: Start simple, but use a structure that can grow.
  • Integrate billing: Stripe is the standard. Use their webhooks religiously.
  • Add audit logging: Track every important action from day one.
  • Build API rate limiting: Use Redis and the token bucket algorithm.

Each of these deserves its own deep dive, but this foundation will serve you better than any todo-app-style example ever could.

Conclusion: Patterns Over Examples

The lack of complete B2B SaaS examples isn't a problem to solve-it's a reality to accept. The real value isn't in seeing someone else's monolithic codebase; it's in understanding the patterns that make B2B SaaS applications work at scale.

Focus on multi-tenancy, authorization, subscription management, and audit logging. Get these right from the start, and you'll avoid the painful rewrites that plague SaaS startups. The code you write today, informed by these patterns, will become your own reference architecture. And it'll be better than any generic example because it'll be tailored to your specific domain and requirements.

Now stop searching for the perfect example and start building. The patterns above are your blueprint.

Comments

No comments yet. Start the discussion.