惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

云风的 BLOG
云风的 BLOG
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
博客园 - 三生石上(FineUI控件)
T
The Blog of Author Tim Ferriss
宝玉的分享
宝玉的分享
Hugging Face - Blog
Hugging Face - Blog
WordPress大学
WordPress大学
V
Visual Studio Blog
小众软件
小众软件
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
MongoDB | Blog
MongoDB | Blog
V
V2EX
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 【当耐特】
Microsoft Azure Blog
Microsoft Azure Blog
The Cloudflare Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Engineering at Meta
Engineering at Meta
L
LangChain Blog
Martin Fowler
Martin Fowler
GbyAI
GbyAI
博客园 - 司徒正美

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How we built multi-tenant isolation in NestJS that even a...
joah levien · 2026-06-18 · via DEV Community
Cover image for How we built multi-tenant isolation in NestJS that even a junior dev can't break

joah levien

About a year ago, a junior dev on our team wrote a cleanup job that nuked records without a tenant filter. In staging,
thankfully — but it wiped out an entire test tenant's data and took half a day to restore. That was the wake-up call.

We run a multi-tenant NestJS + TypeORM SaaS (shared database, shared schema, tenant_id column on everything). The
classic approach is "just remember to add WHERE tenant_id = ? everywhere." Which works great right up until it

doesn't.

So we built a three-layer safety net. Sharing it because I haven't seen this exact combo written up anywhere, and it
took us a few iterations to get right.

## Layer 1: Every entity inherits tenant ownership


typescript                                                                                                         
  @Entity()                                                                                                             
  export abstract class TenantBaseEntity {
    @Column()                                                                                                           
    tenantId: string;                                                                                                 
  }                                       

  Dead simple. If an entity doesn't extend this, it doesn't get created. We enforce this in code review — no exceptions.
   It means the column physically exists on every table, which matters for Layer 3.

  Layer 2: Tenant context lives on the request                                                                          

  @Injectable({ scope: Scope.REQUEST })                                                                                 
  export class TenantService {                                                                                        
    private tenantId: string;             

    constructor(@Inject(REQUEST) private request: Request) {                                                            
      this.tenantId = this.request.user?.tenantId;
    }                                                                                                                   

    getTenantId(): string {
      if (!this.tenantId) {                                                                                             
        throw new Error('Tenant context not available — are you in a non-HTTP context?');                             
      }                                       
      return this.tenantId;               
    }                                                                                                                   
  }                                                                                                                     

  The key addition we made after getting burned: that guard clause. If something tries to query without a tenant        
  context, it throws loudly instead of silently returning unscoped data. Fail closed, not open.                       

  Layer 3: A custom repository that makes forgetting impossible                                                         

  @Injectable()                                                                                                         
  export class TenantRepository<T extends TenantBaseEntity> {                                                           
    constructor(
      private repo: Repository<T>,                                                                                      
      private tenantService: TenantService,                                                                           
    ) {}                                      

    async find(options?: FindManyOptions<T>): Promise<T[]> {
      return this.repo.find({                                                                                           
        ...options,                           
        where: {                                                                                                        
          ...options?.where,                                                                                          
          tenantId: this.tenantService.getTenantId(),                                                                   
        } as any,
      });                                                                                                               
    }                                                                                                                 

    async findOne(options?: FindOneOptions<T>): Promise<T | null> {
      return this.repo.findOne({
        ...options,                                                                                                     
        where: {
          ...options?.where,                                                                                            
          tenantId: this.tenantService.getTenantId(),                                                                 
        } as any,                             
      });                                 
    }

    // same pattern for save, update, delete...
  }                                                                                                                     

  Devs inject TenantRepository<Whatever> instead of the raw TypeORM repo. The tenant filter is injected automatically on
   every operation. You can't forget it because you never write it.

  The edge case that bit us: background jobs                                                                            

  Cron tasks, BullMQ workers — anything outside an HTTP request has no request-scoped context, so TenantService blows   
  up. We solved this with an explicit TenantContext wrapper:                                                            

  await this.tenantContext.runWithTenant(tenantId, async () => {                                                        
    await this.tenantRepository.find();                                                                               
  });                                                                                                                   

  Honest tradeoffs

  Not gonna pretend this is perfect:          

  - Query performance — composite indexes on every table. Our DBA was not thrilled.                                   
  - Request-scoped injection — NestJS creates new instances per request. At scale, look into AsyncLocalStorage with     
  nestjs-cls.                                 
  - Raw queries — if someone writes raw SQL, none of this helps. We lint for query() and createQueryBuilder() in CI.    

  Running in production for about a year across ~40 tables. Zero cross-tenant incidents since.                          

  What's next?                                                                                                          

  Genuinely curious — anyone gone the schema-per-tenant route with NestJS? We evaluated it early but connection pool    
  management seemed nightmarish at ~200 tenants. Also wondering about Postgres RLS as an alternative.                   

  ---                                                                                                                   
  We packaged this pattern (along with auth, Stripe payments, RBAC, admin dashboard, and deployment configs) into a full
   SaaS starter kit:                                                                                                    

  - 🔗 https://github.com/sayahweb2-png/saas-starter-lite (MIT licensed)
  - 🔗 https://demo.cloudrix.io                                                                                         
  - 🔗 https://demo.cloudrix.io/blog/nestjs-angular-authentication-jwt-oauth