npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tenanso

v0.2.3

Published

Multi-tenant SQLite with Drizzle ORM and Turso

Readme

tenanso

Multi-tenant SQLite for TypeScript — database-per-tenant isolation using Drizzle ORM and Turso.

Each tenant gets their own SQLite database managed by Turso. Your application code stays tenant-unaware — tenanso handles connection routing, tenant lifecycle, and framework integration.

Inspired by Rails 8's activerecord-tenanted.

Features

  • Database-per-tenant isolation — each tenant's data is physically separated, no WHERE tenant_id = ? needed
  • Runtime-agnostic — core uses only fetch and Map, no node: imports. Works on Cloudflare Workers, Deno, Bun, and Node.js
  • Turso Platform API integration — create and delete tenant databases dynamically
  • LRU connection pooling — caps memory and file descriptor usage with configurable maxConnections (default 50)
  • Hono middleware — optional peer dependency with first-class integration. import "tenanso" has zero Hono imports
  • Type-safe — full TypeScript support with Drizzle's type inference

Install

npm install tenanso drizzle-orm @libsql/client
# or
pnpm add tenanso drizzle-orm @libsql/client
# or
yarn add tenanso drizzle-orm @libsql/client

If using the Hono middleware:

npm install hono

Quick Start

1. Define your Drizzle schema

// db/schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";

export const users = sqliteTable("users", {
  id: integer("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull(),
});

2. Create a tenanso instance

import { createTenanso } from "tenanso";
import * as schema from "./db/schema.js";

const tenanso = createTenanso({
  turso: {
    organizationSlug: "my-org",
    apiToken: process.env.TURSO_API_TOKEN!,
    group: "my-app",
  },
  databaseUrl: "libsql://{tenant}-my-app-my-account.turso.io",
  authToken: process.env.TURSO_GROUP_AUTH_TOKEN!,
  schema,
  // New tenant databases are cloned from the seed database
  seed: { database: "seed-db" },
  // Pass additional Drizzle options (e.g. casing)
  drizzleOptions: { casing: "snake_case" },
});

3. Use it

// Create a tenant database
await tenanso.createTenant("acme-corp");

// Query a tenant's database
await tenanso.withTenant("acme-corp", async (db) => {
  await db.insert(users).values({ name: "Alice", email: "[email protected]" });
  const allUsers = await db.select().from(users);
});

// Or get a db instance directly
const db = tenanso.dbFor("acme-corp");

Hono Integration

tenanso provides an optional Hono middleware that sets c.var.db and c.var.tenant for each request.

import { Hono } from "hono";
import { contextStorage } from "hono/context-storage";
import { createTenanso } from "tenanso";
import { tenantMiddleware, type TenansoEnv } from "tenanso/hono";

const tenanso = createTenanso({ /* ... */ });
const app = new Hono<TenansoEnv>();

app.use(contextStorage());
app.use("/api/*", tenantMiddleware(tenanso, {
  resolve: (c) => c.req.header("x-tenant-id"),
}));

app.get("/api/users", async (c) => {
  const db = c.var.db;       // DrizzleDb — fully typed
  const tenant = c.var.tenant; // string
  const users = await db.select().from(usersTable);
  return c.json(users);
});

Accessing the db outside handlers

With Hono's contextStorage() middleware enabled, you can access the tenant db from anywhere in the async call stack:

import { getTenantDb, getTenantName } from "tenanso/hono";

async function getActiveUserCount(): Promise<number> {
  const db = getTenantDb();
  const result = await db.select().from(users);
  return result.length;
}

Tenant resolution strategies

The resolve function determines which tenant a request belongs to. Here are common patterns:

// From header
resolve: (c) => c.req.header("x-tenant-id")

// From URL path parameter (/t/:tenantId/*)
resolve: (c) => c.req.param("tenantId")

// From subdomain (acme.myapp.com → "acme")
resolve: (c) => {
  const url = new URL(c.req.url);
  return url.hostname.split(".")[0];
}

// From a verified JWT claim
resolve: (c) => {
  const payload = c.get("jwtPayload");
  return payload.tenant;
}

Authentication

tenanso handles tenant resolution, not authentication. Auth is your application's responsibility, but how you wire them together matters for security.

The tenant must come from a verified source. Never trust a raw client header without authentication.

Recommended: JWT with tenant claim

import { jwt } from "hono/jwt";

// 1. Verify JWT first
app.use("/api/*", jwt({ secret: "your-secret", alg: "HS256" }));

// 2. Resolve tenant from the verified payload
app.use("/api/*", tenantMiddleware(tenanso, {
  resolve: (c) => c.get("jwtPayload").tenant,
}));

External auth provider (Clerk, Auth0)

app.use("/api/*", clerkMiddleware());
app.use("/api/*", tenantMiddleware(tenanso, {
  resolve: (c) => c.get("clerkAuth").tenantSlug,
}));

API key

app.use("/api/*", async (c, next) => {
  const key = c.req.header("Authorization")?.slice(7);
  const tenant = await lookupTenantByApiKey(key);
  if (!tenant) return c.json({ error: "Invalid API key" }, 401);
  c.set("resolvedTenant", tenant);
  await next();
});

app.use("/api/*", tenantMiddleware(tenanso, {
  resolve: (c) => c.get("resolvedTenant"),
}));

API Reference

createTenanso(config)

Creates a tenanso instance.

const tenanso = createTenanso({
  turso: {
    organizationSlug: string;   // Turso org slug
    apiToken: string;           // Turso Platform API token
    group: string;              // Database group (e.g. "my-app")
  },
  databaseUrl: string;          // URL template: "libsql://{tenant}-my-app-my-account.turso.io"
  authToken: string;            // Turso group auth token
  schema: Record<string, unknown>; // Drizzle schema
  seed?: { database: string };  // Clone new tenants from this database
  maxConnections?: number;      // Max cached connections (default: 50)
  drizzleOptions?: Record<string, unknown>; // Additional options passed to drizzle()
});

TenansoInstance

| Method | Description | |---|---| | dbFor(tenant) | Returns a cached DrizzleDb instance for the tenant | | withTenant(tenant, fn) | Runs a callback with the tenant's DrizzleDb | | createTenant(name) | Creates a new database via Turso Platform API | | deleteTenant(name) | Deletes a database via Turso Platform API | | listTenants() | Lists all databases in the organization | | tenantExists(name) | Checks if a tenant database exists |

tenantMiddleware(tenanso, options) (from tenanso/hono)

Hono middleware that resolves the tenant from the request and sets c.var.db and c.var.tenant.

Returns 400 if resolve returns undefined.

getTenantDb() / getTenantName() (from tenanso/hono)

Access the current tenant's db or name from outside Hono handlers. Requires Hono's contextStorage() middleware.

Turso Setup

Create a group

Use a group per application to organize databases:

turso group create my-app --location nrt
turso group tokens create my-app  # save as TURSO_GROUP_AUTH_TOKEN

Create a seed database

New tenant databases are cloned from a seed database that has your schema already applied:

turso db create seed-db --group my-app
npx drizzle-kit push --url libsql://seed-db-my-app-my-account.turso.io --auth-token $TURSO_GROUP_AUTH_TOKEN

See the Turso Setup guide for more details.

License

MIT