Building business software (SaaS) is different from consumer apps.
You aren't just managing users; you are managing Teams (or Organizations/Tenants).
In this tutorial, we will build "TaskFlow," a Trello-like project management tool where:
- Users can belong to multiple Organizations (e.g., "Work", "Side Project").
- Each Organization has its own Projects and Billing.
- Data must be strictly isolated (Tenant A cannot see Tenant B's data).
Phase 1: The Multi-Tenant Data Model
The database schema is the foundation of multi-tenancy.
Every resource must "belong" to an organization.
// schema.prisma
model Organization {
id String @id @default(uuid())
name String
slug String @unique // e.g., taskflow.com/acme-corp
members OrgMember[]
projects Project[]
}
model OrgMember {
id String @id @default(uuid())
orgId String
userId String // The Rugi Auth User ID
role String // 'OWNER', 'MEMBER', 'GUEST'
organization Organization @relation(fields: [orgId], references: [id])
@@unique([orgId, userId]) // User can join an org only once
}
model Project {
id String @id @default(uuid())
orgId String
title String
tasks Task[]
// CRITICAL: Linking project to Org ensures isolation
organization Organization @relation(fields: [orgId], references: [id])
}
Phase 2: Resolving Identity vs. Context
Here is the conceptual leap:
Identity is "Who are you?" (I am Alice).
Context is "Where are you?" (I am currently working in Acme Corp).
Many developers make the mistake of creating a new user account for every organization (alice@acme.com, alice@beta.com). This is a nightmare for users.
The Better Way (Powered by Rugi Auth):
- Alice has One Identity in Rugi Auth.
- Your application handles the Context.
Step 1: Authentication (Identity)
Alice logs in via Rugi Auth. We get her userId.
Step 2: Tenant Resolution (Context)
When Alice visits app.taskflow.com/acme-corp/dashboard:
- Middleware Check: Is Alice authenticated? (Yes, valid Token).
- Membership Check:
Does
OrgMembertable have an entry foruserId: AliceANDorgId: AcmeCorp?- Yes: Proceed.
- No: 403 Forbidden.
// middleware/tenant.ts
import { prisma } from './db';
export const requireTenantMembership = async (req, res, next) => {
const userId = req.user.id;
const orgSlug = req.params.orgSlug; // from URL
const member = await prisma.orgMember.findFirst({
where: {
userId,
organization: { slug: orgSlug }
}
});
if (!member) {
return res.status(403).json({ error: "You are not a member of this organization" });
}
// Attach membership info to request for downstream use
req.memberRole = member.role;
req.orgId = member.orgId;
next();
};
Phase 3: Invitation System (Growth Engine)
SaaS grows via invites. "Alice invites Bob to Acme Corp."
- Frontend: Alice enters Bob's email in the dashboard.
- Backend:
- Check if Bob already has a Rugi Auth account?
- If Yes: Add entry to
OrgMember. Send email "You've been added!". - If No: create a "Pending Invite".
Using Rugi Auth's Registration API:
You can pre-provision users or simply send them to your registration page.
When Bob finally registers with that email, your webhook (or post-registration hook) detects the pending invite and automatically adds him to the organization.
Phase 4: Billing & Admin (Global Scope)
You, as the SaaS builder, need a "God Mode" to see who is paying.
In Rugi Auth, define a role SAAS_SUPER_ADMIN in your app configuration.
// routes/billing.ts
// Only for YOU
router.get('/all-tenants', requireRole('SAAS_SUPER_ADMIN'), async (req, res) => {
const allOrgs = await prisma.organization.findMany({
include: { _count: { select: { members: true } } }
});
res.json(allOrgs);
});
This keeps your "Super Admin" logic completely separate from the "Organization Owner" logic, preventing the accidental "Customer A sees Customer B" bugs.
Summary
Building a SaaS is about rigorous data isolation.
- Rugi Auth handles the global "Who is this person?" question.
- Your App handles the huge "Which team are they on?" matrix.
This separation allows your SaaS to support users who belong to 50 different teams with a single login, a feature users love and expect from modern tools.
Top comments (0)