Build the library from core outward, ending in a runnable demo against real Postgres.
ORM-agnostic: registry, cache, context, resolvers. No framework dependencies.
-
TenantRegistry— create / resolve / suspend / delete / update tenants -
ConnectionCache— LRU eviction, idle-timeout sweeping, per-tenant stats -
TenantContext—AsyncLocalStorage,runWithTenant(),useTenant(),getCurrentTenant() - Resolvers —
SubdomainResolver,HeaderResolver,PathResolver - Types —
MasterStore,Adapter,Cipher,LifecycleHooks,Resolver - Full Vitest suite — 52/52 tests passing
- In-memory fakes —
InMemoryMasterStore,FakeAdapter(used in tests)
Sequelize v6 adapter — connection factory, model registration, master store.
-
SequelizeMasterStore— implementsMasterStoreusing a Sequelize model (Tenanttable) -
SequelizeAdapter— implementsAdapter<Sequelize>, handles instance creation, dialect config, pool config, model factory application, graceful disconnect -
AdapterContexttype —{ sequelize: Sequelize }passed to model factories - Full Vitest suite — 20/20 tests with SQLite in-memory master DB
Thin Express middleware — resolves tenant, runs handler inside
runWithTenant.
-
tenantMiddleware(options)— calls resolver, callsregistry.resolveBySlug, callsrunWithTenant, callsnext() - Error handling — 404 on
TenantNotFoundError, 403 onTenantNotActiveError, 500 on unexpected errors - Custom
onMissingTenanthook - Accepts
Resolverinstance or plain async/sync function - Full Vitest suite — 10/10 tests (including concurrent context isolation)
Runnable demo — 3 tenants in Postgres via Docker Compose.
-
docker-compose.yml— master Postgres + 3 tenant Postgres instances - Subdomain resolution demo —
acme.localhost,globex.localhost,initech.localhost - Model usage —
Ordermodel with CRUD routes (list, create, delete) - Tenant provisioning script (
pnpm provision) — idempotent, syncs schema viaonCreatehook -
GET /health— cache size + per-tenant stats - README — how to run locally
@kosan/cli— runs Sequelize migrations across all active tenants.
-
kosan migratecommand — iterates active tenants, runs migrations per tenant -
--concurrency Nflag — parallel migrations with failure isolation (per-tenant errors captured, siblings continue) -
--tenant <slug>flag — target a single tenant -
KosanConfigtype —master,migrationsPath,migrationsTableName,concurrency - Config loaded via
jiti(supports.ts,.js,.mjsconfig files) - Per-tenant result summary —
printResults()table with outcome, applied count, duration -
Migrationinterface —name,up(qi),down(qi)— decoupled from file loading -
loadMigrationsFromDir()— reads directory, sorts alphabetically, lazy dynamic import -
withConcurrency()pool — bounded parallel execution, results in input order - Tests — 16/16 (7 pool tests, 9 runner/printer tests with in-memory SQLite)
@kosan/prisma— adapter for Prisma Client.
-
PrismaAdapter— onePrismaClientper tenant, datasource URL override viabuildUrl -
PrismaMasterStore— master tenant table via structuralTenantDelegatetype (passprisma.tenant) -
usePrisma<TClient>()— typed shortcut that returns the client fromuseTenant().models.prisma - No hard dependency on
@prisma/client— fully generic, peer dep only -
TENANT_PRISMA_SCHEMAexport — copy-pasteable Prisma schema snippet -
PrismaClientConstructortype — typed constructor interface for the generated client - Tests — 26/26 (15 store tests, 11 adapter+context tests, full registry integration)
-
@kosan/fastify— Fastify plugin (callback-basedonRequesthook;wrapWithTenantContextfor correct async propagation) -
@kosan/koa— Koa middleware (runWithTenantwrappingnext()— works cleanly with Koa's Promise chain) - Core addition:
wrapWithTenantContext(value, callback)— callscallbackinsidestorage.run()for frameworks that can't use async closures - Tests — 10 Fastify + 11 Koa (happy path, errors, resolver types, concurrent isolation)
-
CacheStatsextended withtenantSlugandidleMsfields -
ConnectionCache.maxSizegetter — publicly readable -
TenantRegistry.getStats()— returns{ cacheSize, cacheMaxSize, entries } -
getTenantLogContext()— reads current tenant fromAsyncLocalStorage, returns{ tenantId, tenantSlug }(empty object outside context) -
getHealthPayload(registry)— builds a serialisable health payload from cache state -
SlowQueryInfotype +onSlowQuerycallback +slowQueryThresholdMsoption inSequelizeAdapterOptions - Slow-query detection in
SequelizeAdaptervia wrappedlogging+benchmark: true - Tests — 7 observability tests in
@kosan/core, 5 slow-query tests in@kosan/sequelize - All exports added to
@kosan/coreand@kosan/sequelizeindex files
VitePress documentation.
- Getting started guide
- API reference (auto-generated from TypeScript)
- Adapter authoring guide
- Deployment guide (subdomain setup, credential encryption, Docker)
- Migration guide
@kosan/nestjs— NestJS module, guard, and decorator integration.
-
KosanModule.forRoot— synchronous registration -
KosanModule.forRootAsync— async registration (useFactory, inject) -
TenantMiddleware— NestJS middleware wrappingrunWithTenant(ctx, next) -
TenantGuard— optional guard asserting tenant context is present -
@CurrentTenant()— parameter decorator returningTenantContextValue -
@InjectRegistry()— constructor decorator injecting theTenantRegistry - Tests — 16/16 (KosanModule structure, TenantGuard, TenantMiddleware with context isolation)
- Example —
examples/sequelize-nestjs/— runnable NestJS app with Docker Compose, 3 tenants - Docs —
docs/docs/packages/nestjs.mdupdated +docs/docs/examples/sequelize-nestjs.md