Episode 43 membangun aplikasi multi-tenant SaaS: arsitektur database per tenant versus shared database, tenant context propagation, manajemen organisasi dengan undangan dan role, billing dengan Stripe subscriptions, hingga fitur enterprise seperti SSO dan audit logs.

Project terakhir di fase real-world: aplikasi multi-tenant SaaS — satu aplikasi melayani banyak organisasi pelanggan dengan isolasi data dan kebijakan billing yang aman. Ini pola arsitektur yang dipakai hampir semua perusahaan SaaS modern. Episode 43 membangunnya dari fondasi multi-tenancy sampai fitur enterprise: arsitektur isolasi, propagasi tenant context, manajemen organisasi, billing, dan fitur seperti SSO serta audit logs.
model Project {
id Int @id @default(autoincrement())
tenantId Int
name String
@@index([tenantId])
}Untuk seri ini, kita memakai shared database dengan tenantId pada setiap tabel — pola paling umum untuk SaaS.
tenantId.Query: {
projects: async (_, __, ctx) => {
return ctx.prisma.project.findMany({
where: { tenantId: ctx.tenant.id },
});
},
},Paling penting: tenant ditentukan dari context (hasil autentikasi), bukan dari argument yang dikirim client — jika client bisa memilih tenant, isolasi runtuh.
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 };
},Setiap resolver membaca ctx.tenant dan ctx.role, misalnya ctx.tenant.id. Ini membuat propagasi tenant otomatis ke seluruh query dan mutation tanpa kode berulang.
Organisasi (tenant) punya anggota dengan role — ini memakai pola RBAC dari episode 14:
type Organization {
id: ID!
name: String!
members: MemberConnection!
plans: [Plan!]!
}
type Member {
user: User!
role: OrgRole!
}
enum OrgRole {
OWNER
ADMIN
MEMBER
VIEWER
}Undangan anggota memakai pola: invite dibuat dengan email + token, calon anggota menerima link, dan mengklaim keanggotaan saat signup atau join. Hak istimewa dibatasi: hanya OWNER dan ADMIN yang bisa mengundang dan mengubah role — pemeriksaan memakai ctx.role.
Billing memakai Stripe subscriptions, bukan checkout sekali bayar; install dengan npm install stripe:
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 };
}Untuk usage-based billing, kirim metrik pemakaian ke Stripe secara berkala; plan management membandingkan plan.quota dengan pemakaian saat ini dan menampilkan upgrade prompt saat melewati batas. Invoice generation ditangani Stripe; timpa template bila perlu branding khusus.
type AuditLog {
id: ID!
actor: User!
action: String!
resource: String!
tenantId: ID!
createdAt: DateTime!
}Tulis audit log dari resolver yang menangani aksi sensitif (update role, delete data, perubahan billing).
const quota = await ctx.redis.get(`quota:${ctx.tenant.id}`);
if (Number(quota) >= ctx.tenant.plan.limit) {
throw new Error("Kuota bulanan habis");
}Inti yang harus dibawa pulang:
Di episode 44 selanjutnya kalian akan mempelajari API design patterns dan best practices — naming conventions, konsistensi struktur, standar dokumentasi, best practices performa dan keamanan, hingga strategi evolusi schema. Kalian akan merancang API yang production-ready dari nol!