Tenant data leaks between tenants = catastrophic bug. It's not just a data breach — it destroys trust and can end your SaaS business overnight. Claude Code generates tenant isolation automatically from CLAUDE.md rules, so you never accidentally expose one tenant's data to another.
CLAUDE.md Tenant Isolation Rules
Add these rules to your CLAUDE.md and Claude Code enforces them across every file it touches:
## Database Security Rules
- All DB queries must include tenantId filter
- tenantId comes from JWT (never from request params)
- All tables need tenantId NOT NULL constraint
- Middleware sets req.tenant on every request
- BaseRepository auto-applies tenant filter
- Use AsyncLocalStorage for context propagation
- PostgreSQL RLS as backup isolation layer
Generating Tenant Isolation Code
Prompt:
Create tenant isolation middleware and base repository using AsyncLocalStorage.
Follow CLAUDE.md database security rules.
AsyncLocalStorage Middleware
Claude Code generates context-safe tenant propagation:
import { AsyncLocalStorage } from 'async_hooks';
const tenantContext = new AsyncLocalStorage<{ tenantId: string }>();
export function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
// tenantId comes from JWT — never trust request params
tenantContext.run({ tenantId: payload.tenantId }, () => {
req.tenant = { id: payload.tenantId };
next();
});
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}
export function getCurrentTenantId(): string {
const store = tenantContext.getStore();
if (!store) throw new Error('TenantContext not initialized — missing middleware?');
return store.tenantId;
}
The getCurrentTenantId() function throws if called outside the middleware context. This means a missing tenantMiddleware causes an immediate error — not a silent data leak.
BaseRepository with Auto-Applied Tenant Filter
abstract class BaseRepository<T> {
protected abstract tableName: string;
protected get tenantId(): string {
return getCurrentTenantId(); // throws if context missing
}
async findById(id: string): Promise<T | null> {
return prisma[this.tableName].findFirst({
where: { id, tenantId: this.tenantId }, // always filtered
});
}
async findMany(where: Partial<T> = {}): Promise<T[]> {
return prisma[this.tableName].findMany({
where: { ...where, tenantId: this.tenantId },
});
}
async create(data: Omit<T, 'tenantId' | 'id'>): Promise<T> {
return prisma[this.tableName].create({
data: { ...data, tenantId: this.tenantId }, // injected automatically
});
}
async update(id: string, data: Partial<T>): Promise<T> {
// Check ownership before updating
const existing = await this.findById(id);
if (!existing) throw new Error('Not found or access denied');
return prisma[this.tableName].update({
where: { id },
data,
});
}
}
Every method automatically scopes to the current tenant. Developers can't forget the filter — it's baked into the base class.
UserRepository Example
class UserRepository extends BaseRepository<User> {
protected tableName = 'user';
async findByEmail(email: string): Promise<User | null> {
return this.findMany({ email }).then(users => users[0] ?? null);
// tenantId filter applied automatically by findMany
}
async updateProfile(userId: string, profile: UpdateProfileDto): Promise<User> {
return this.update(userId, profile);
// ownership check + tenant filter both applied by update()
}
}
No explicit tenantId in application code — the base class handles it all.
PostgreSQL RLS as Backup Layer
Even if application code has a bug, RLS prevents cross-tenant access at the database level:
-- Enable RLS on all tenant-scoped tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
-- Policy: rows only visible when tenant_id matches session variable
CREATE POLICY tenant_isolation ON users
USING (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.tenant_id')::uuid);
Prisma middleware sets the session variable before every query:
prisma.$use(async (params, next) => {
const tenantId = getCurrentTenantId();
await prisma.$executeRaw`SELECT set_config('app.tenant_id', ${tenantId}, true)`;
return next(params);
});
Now even raw SQL queries or direct DB connections respect tenant boundaries.
Defense in Depth Summary
| Layer | Mechanism | What It Prevents |
|---|---|---|
| JWT | tenantId from token | Tenant spoofing via params |
| AsyncLocalStorage | Context propagation | Accidental tenantId omission |
| BaseRepository | Auto-applied filter | Missing WHERE clauses |
| PostgreSQL RLS | DB-level policy | Bypassed application logic |
Claude Code generates all four layers from a single CLAUDE.md file. Add the rules once — get consistent enforcement across the entire codebase.
Security Pack (¥1,480) — Use /security-check to scan your SaaS for tenant isolation vulnerabilities, missing RLS policies, and JWT misconfiguration.
Available at prompt-works.jp
Top comments (0)