Learn GraphQL - Project: Enterprise SaaS Application
Episode 43 of 51

Learn GraphQL - Project: Enterprise SaaS Application

Episode 43 builds a multi-tenant SaaS application: per-tenant vs shared database architecture, tenant context propagation, organization management with invitations and roles, billing with Stripe subscriptions, and enterprise features like SSO and audit logs.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

The last project in the real-world phase: a multi-tenant SaaS application — one app serving many customer organizations with secure data isolation and billing policies. This is the architecture pattern nearly every modern SaaS company uses. Episode 43 builds it from the multi-tenancy foundation to enterprise features: isolation architecture, tenant context propagation, organization management, billing, and features like SSO and audit logs.

Multi-Tenancy Architecture

Database per Tenant vs Shared Database

  • Database per tenant: strongest isolation, easy per-tenant backups, but expensive and hard to scale to thousands of tenants.
  • Shared database with tenant_id: cost-effective and easy to scale, but isolation depends on query discipline.
  • Hybrid: a shared cluster for small tenants, dedicated databases for large enterprises.
JSShared model with tenantId
model Project {
  id       Int    @id @default(autoincrement())
  tenantId Int
  name     String
  @@index([tenantId])
}

For this series, we'll use a shared database with tenantId on every table — the most common SaaS pattern.

Tenant Isolation Strategies

  • Schema-level: ensure every query includes a tenantId filter.
  • Resolver-level: derive the tenant from context, not from client input.
  • Database-level: row-level security when the database supports it.
JSTenant filter in every query
Query: {
  projects: async (_, __, ctx) => {
    return ctx.prisma.project.findMany({
      where: { tenantId: ctx.tenant.id },
    });
  },
},

Most importantly: the tenant is determined from context (the authentication result), not from arguments sent by the client — if a client can choose its tenant, isolation collapses.

Tenant Context Propagation

JSTenant in context
context: async ({ req }) => {
  const user = await authenticate(req.headers.authorization);
  if (user) {
    const membership = await findMembership(user.id);
    return { user, tenant: membership.tenant, role: membership.role };
  }
  return { user: null, tenant: null, role: null };
},

Every resolver reads ctx.tenant and ctx.role, for example ctx.tenant.id. This propagates the tenant automatically across all queries and mutations without repeated code.

Organization Management

Team Structure and Roles

An organization (tenant) has members with roles — this uses the RBAC pattern from episode 14:

Organization schema
type Organization {
  id: ID!
  name: String!
  members: MemberConnection!
  plans: [Plan!]!
}
 
type Member {
  user: User!
  role: OrgRole!
}
 
enum OrgRole {
  OWNER
  ADMIN
  MEMBER
  VIEWER
}

Invitation System and Subscription

Member invitations use the pattern: an invite is created with email + token, the prospective member receives a link, and claims membership at signup or join. Privileges are limited: only OWNER and ADMIN can invite and change roles — checked with ctx.role.

Billing and Subscriptions

Stripe Subscription Integration

Billing uses Stripe subscriptions, not one-time checkout; install with npm install stripe:

JSCreate a Stripe subscription
import Stripe from "stripe";
 
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
 
async function createSubscription(_, args, ctx) {
  const customer = await stripe.customers.create({
    email: ctx.user.email,
    metadata: { tenantId: String(ctx.tenant.id) },
  });
 
  const subscription = await stripe.subscriptions.create({
    customer: customer.id,
    items: [{ price: args.input.priceId }],
    payment_settings: { save_default_payment_method: "on_subscription" },
  });
 
  return { subscriptionId: subscription.id };
}

Usage-Based Billing and Plan Management

For usage-based billing, send usage metrics to Stripe periodically; plan management compares plan.quota with current usage and shows an upgrade prompt when limits are exceeded. Invoice generation is handled by Stripe; override the template when custom branding is needed.

Enterprise Features

SSO (SAML and OAuth) and Audit Logs

  • SSO: SAML and OIDC support for companies — usually via a provider like Okta or Auth0. After SSO login, the tenant is mapped by email domain.
  • Audit logs: record all important actions (who, what, when, which tenant) — mandatory for compliance.
Audit log entry
type AuditLog {
  id: ID!
  actor: User!
  action: String!
  resource: String!
  tenantId: ID!
  createdAt: DateTime!
}

Write audit logs from resolvers that handle sensitive actions (role updates, data deletion, billing changes).

Per-Tenant Rate Limits and White-labeling

  • Per-tenant API rate limits: each tenant has a different quota per plan (episode 15).
  • White-labeling: logo and color configuration per tenant, stored as settings.
  • Custom domains: map domains to tenants for direct access.
JSPer-tenant quota
const quota = await ctx.redis.get(`quota:${ctx.tenant.id}`);
if (Number(quota) >= ctx.tenant.plan.limit) {
  throw new Error("Kuota bulanan habis");
}

Conclusion

Key takeaways:

  • Multi-tenancy uses per-tenant databases, a shared database, or a hybrid.
  • The tenant is derived from context, not client input — the key to isolation.
  • Organizations and roles use the RBAC pattern with a hierarchy.
  • Stripe subscriptions handle billing and usage-based pricing.
  • SSO, audit logs, and per-tenant rate limits are mandatory enterprise features.
  • Every query is infused with a tenantId filter to keep isolation intact.

In the next episode, episode 44, you'll learn about API design patterns and best practices — naming conventions, structural consistency, documentation standards, performance and security best practices, and schema evolution strategies. You'll design production-ready APIs from scratch!

Learn GraphQL - Project: Enterprise SaaS Application | Learn GraphQL