Drizzle <> Nile

This guide assumes familiarity with:

According to the official website, Nile is PostgreSQL re-engineered for multi-tenant apps.

Checkout official Nile + Drizzle Quickstart and **Migration docs.

You can use Nile with any of Drizzle’s Postgres drivers, we’ll be showing the use of node-postgres below.

Step 1 - Install packages

npm
yarn
pnpm
bun
npm i drizzle-orm postgres
npm i -D drizzle-kit

Step 2 - Initialize the driver and make a query

index.ts
// Make sure to install the 'pg' package
import { drizzle } from 'drizzle-orm/node-postgres'

const db = drizzle(process.env.NILEDB_URL);

const response = await db.select().from(...);

If you need to provide your existing driver:

index.ts
// Make sure to install the 'pg' package
import { pgTable, serial, text, varchar } from "drizzle-orm/pg-core";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});
const db = drizzle({ client: pool });

const response = await db.select().from(...);

Connecting to a virtual tenant database

Nile provides virtual tenant databases, when you set the tenant context, Nile will direct your queries to the virtual database for this particular tenant and all queries will apply to that tenant (i.e. select * from table will result records only for this tenant).

In order to set the tenant context, we wrap each query in a transaction that sets the appropriate tenant context before running the transaction.

The tenant ID can simply be passed into the wrapper as an argument:

index.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { todosTable, tenants } from "./db/schema";
import { sql } from 'drizzle-orm';
import 'dotenv/config';

const db = drizzle(process.env.NILEDB_URL);

function tenantDB<T>(tenantId: string, cb: (tx: any) => T | Promise<T>): Promise<T> {
  return db.transaction(async (tx) => {
    if (tenantId) {
      await tx.execute(sql`set local nile.tenant_id = '${sql.raw(tenantId)}'`);
    }

    return cb(tx);
  }) as Promise<T>;
}

// In a webapp, you'll likely get it from the request path parameters or headers
const tenantId = '01943e56-16df-754f-a7b6-6234c368b400'

const response = await tenantDB(tenantId, async (tx) => {
    // No need for a "where" clause here
    return await tx.select().from(todosTable);
});

console.log(response);

If you are using a web framwork that supports it, you can set up AsyncLocalStorage and use middleware to populate it with the tenant ID. In this case, your Drizzle client setup will be:

import { drizzle } from 'drizzle-orm/node-postgres';
import dotenv from "dotenv/config";
import { sql } from "drizzle-orm";
import { AsyncLocalStorage } from "async_hooks";

export const db = drizzle(process.env.NILEDB_URL);
export const tenantContext = new AsyncLocalStorage<string | undefined>();

export function tenantDB<T>(cb: (tx: any) => T | Promise<T>): Promise<T> {
  return db.transaction(async (tx) => {
    const tenantId = tenantContext.getStore();
    console.log("executing query with tenant: " + tenantId);
    // if there's a tenant ID, set it in the transaction context
    if (tenantId) {
      await tx.execute(sql`set local nile.tenant_id = '${sql.raw(tenantId)}'`);
    }

    return cb(tx);
  }) as Promise<T>;
}

And then, configure a middleware to populate the the AsyncLocalStorage and use tenantDB method when handling requests:

app.ts
// Middleware to set tenant context
app.use("/api/tenants/:tenantId/*", async (c, next) => {
  const tenantId = c.req.param("tenantId");
  console.log("setting context to tenant: " + tenantId);
  return tenantContext.run(tenantId, () => next());
});

// Route handler
app.get("/api/tenants/:tenantId/todos", async (c) => {
    const todos = await tenantDB(c, async (tx) => {
      return await tx
        .select({
          id: todoSchema.id,
          tenant_id: todoSchema.tenantId,
          title: todoSchema.title,
          estimate: todoSchema.estimate,
        })
        .from(todoSchema);
    });
    return c.json(todos);
});

What’s next?