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

推荐订阅源

D
Docker
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - Franky
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
A
About on SuperTechFans
博客园 - 【当耐特】
Microsoft Security Blog
Microsoft Security Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
The GitHub Blog
The GitHub Blog
雷峰网
雷峰网
博客园_首页
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
IT之家
IT之家
博客园 - 叶小钗
Google DeepMind News
Google DeepMind News
aimingoo的专栏
aimingoo的专栏
博客园 - 聂微东
B
Blog RSS Feed
H
Help Net Security
Recent Announcements
Recent Announcements
阮一峰的网络日志
阮一峰的网络日志
D
DataBreaches.Net
L
LangChain Blog
Vercel News
Vercel News

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
Multi-Tenancy in Bun/Hono Without Boilerplate
Marc · 2026-06-25 · via DEV Community

Marc

Every multi-tenant SaaS has the same problem: you need to make sure every query only returns data for the right tenant. Forget a WHERE tenant_id = ? once, and you have a data leak.

The obvious solution — a separate database per tenant — doesn't scale. Connections are expensive, migration overhead multiplies, and you lose cross-tenant reporting.

For Kumiko we went a different route: a single DB pool, but every query automatically gets the tenantId injected — without handler code ever having to do it manually.

The Idea: TenantDb Instead of a Raw DbRunner

Instead of passing a raw DB connection around, we create a TenantDb wrapper per request:

const db = createTenantDb(rawDb, tenantId)

From that point, db behaves like a normal database — but with automatic isolation baked in:

// Handler code — no tenantId needed
const users = await ctx.db.selectMany(usersTable)
// → SELECT * FROM users WHERE tenant_id IN ('tenant-123', 'system')

await ctx.db.insertOne(usersTable, { name: 'Max' })
// → INSERT INTO users (name, tenant_id) VALUES ('Max', 'tenant-123')

await ctx.db.updateMany(usersTable, { name: 'Moritz' }, { id: userId })
// → UPDATE users SET name='Moritz' WHERE id=? AND tenant_id='tenant-123'

Handlers write plain CRUD code. Isolation happens underneath — invisible, but enforced.

How the Injection Works

Reads: own rows + reference data

Read queries always see two tenants: the current one and SYSTEM_TENANT_ID. This allows reference data (e.g. global config) to be visible to all tenants without duplicating it:

// tenantId filter === [currentTenantId, SYSTEM_TENANT_ID]

If a handler passes its own tenantId in the WHERE clause, it can only narrow the scope, never widen it. A where: { tenantId: 'other-tenant' } is silently dropped.

Writes: own rows only

Inserts get tenantId forced in — and the value cannot be overridden by the caller:

// mode === "tenant": tenantId on INSERT is enforced last
return { ...data, tenantId }  // overwrites whatever the handler passed

Updates and deletes without a WHERE clause throw an error instead of hitting all rows:

// Prevents accidental mass-updates
"TenantDb.updateMany without where would mass-update all tenant rows."

System mode for operators

For admin screens there's r.systemScope() — queries run unfiltered across all tenants. Explicit opt-in only, never the default.

Tenant Resolution in Hono

TenantDb needs a tenantId. It comes from middleware that resolves it from the request — either from the hostname (for custom domains) or from the JWT:

// Middleware (simplified)
app.use('*', async (c, next) => {
  const tenant = await resolveTenant(c.req)
  c.set('db', createTenantDb(rawDb, tenant.id))
  await next()
})

Every handler then gets proper isolation through ctx.db — without a single line of tenant logic in actual feature code.

What This Means in Practice

Across three years of production use and several apps (CashColt, publicstatus, kumiko-studio) we've had zero data leak bugs from forgotten tenant filters. Not because we were particularly careful, but because it's structurally impossible to forget.

The overhead: nearly zero. A few extra conditions per query, no extra DB connection pool, no migration overhead multiplied per tenant.

If you want to try the framework: kumiko.rocks — open source under BUSL-1.1.