Quick Start
This guide walks through a complete Sequelize + Express setup in five minutes. By the end you will have:
- A master database that stores tenant records
- Per-tenant Postgres connections managed automatically
- Express middleware that resolves the tenant from the subdomain
- A route handler that reads from the correct tenant database without any extra configuration
1 — Master database connection
ts
import { TenantRegistry } from '@kosan/core';
import { SequelizeAdapter, SequelizeMasterStore } from '@kosan/sequelize';
import { Sequelize } from 'sequelize';
const master = new Sequelize(
process.env.MASTER_DATABASE_URL ?? 'postgres://admin:pass@localhost/master',
{ logging: false },
);
// Creates the `tenants` table on first run (safe to call every startup).
const masterStore = await SequelizeMasterStore.create(master);
export const registry = await TenantRegistry.create({
master: masterStore,
adapter: new SequelizeAdapter({
defaultDialect: 'postgres',
pool: { max: 5, idle: 30_000 },
}),
cache: {
maxSize: 100, // keep at most 100 open connections
idleTimeoutMs: 1800_000, // evict after 30 min of inactivity
},
});2 — Register a tenant
You only do this once per tenant (e.g. during onboarding):
ts
import { registry } from './registry.js';
const tenant = await registry.createTenant({
slug: 'acme', // used for subdomain resolution
host: 'localhost',
port: 5432,
dbName: 'acme_db',
user: 'acme_user',
password: 'acme_pass',
meta: { plan: 'pro' }, // any JSON you like
});
console.log(tenant.id); // UUID assigned by Kosan3 — Define models
Model factories are registered once on the registry and called per-connection:
ts
import { DataTypes } from 'sequelize';
import type { AdapterContext } from '@kosan/sequelize';
export function OrderModel({ sequelize }: AdapterContext) {
return sequelize.define('Order', {
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
description: { type: DataTypes.STRING, allowNull: false },
total: { type: DataTypes.DECIMAL(10, 2), allowNull: false },
status: { type: DataTypes.ENUM('pending', 'paid', 'cancelled'), defaultValue: 'pending' },
});
}ts
import { OrderModel } from './models/order.js';
registry.registerModels([OrderModel]);4 — Express middleware
ts
import express from 'express';
import { tenantMiddleware } from '@kosan/express';
import { SubdomainResolver } from '@kosan/core';
import { registry } from './registry.js';
export const app = express();
app.use(express.json());
// Resolve tenant from subdomain: acme.myapp.com → slug "acme"
app.use(
tenantMiddleware({
registry,
resolver: new SubdomainResolver(),
onMissingTenant: (_req, res) => {
res.status(400).json({ error: 'No tenant identifier in request.' });
},
}),
);5 — Use models in a route handler
ts
import { useTenant } from '@kosan/core';
import type { ModelStatic, Model } from 'sequelize';
import { app } from '../app.js';
app.get('/orders', async (_req, res) => {
const { models } = useTenant();
const Order = models['Order'] as ModelStatic<Model>;
const orders = await Order.findAll({ order: [['createdAt', 'DESC']] });
res.json(orders);
});
app.post('/orders', async (req, res) => {
const { models } = useTenant();
const Order = models['Order'] as ModelStatic<Model>;
const order = await Order.create(req.body);
res.status(201).json(order);
});6 — Provision a new tenant database on creation
Use the onCreate lifecycle hook to run database creation and initial migrations automatically:
ts
import { registry } from './registry.js';
const registry = await TenantRegistry.create({
master: masterStore,
adapter,
hooks: {
async onCreate(tenant, conn) {
// conn is a fresh Sequelize instance connected to the new tenant DB.
await conn.sync({ force: false }); // or run your migration tool here
},
},
});7 — Start the server
ts
import { app } from './app.js';
app.listen(3000, () => {
console.log('Listening on :3000');
});What happens on a request
tenantMiddlewarecallsSubdomainResolver.resolve(req)→ returns"acme"registry.resolveBySlug("acme")looks up the tenant row in the master DBConnectionCacheopens a new Sequelize connection (or returns the cached one)- The context
{ tenant, connection, models }is stored inAsyncLocalStorage next()is called — your handler runs inside that contextuseTenant()reads the context and returns the right models, zero prop-drilling
Next steps
- Tenant resolvers — header, path, JWT, custom
- Lifecycle hooks — provision, teardown, suspend
- Credential encryption — store passwords with KMS
- CLI migrations — run schema changes across all tenants