> hypequery

Multi-Tenancy

Configure tenant extraction and isolation in the serve({ queries }) runtime.

Multi-Tenancy Isolation

If you need the older builder-chained tenant APIs such as .tenant(...) and .tenantOptional(), see v0.1.x Multi-Tenancy.

Tenant configuration is enforced by the serve({ queries }) runtime and applies to the query definitions you expose there.

Model

  • extract a tenant ID from ctx.auth
  • reject requests when tenant context is required but missing
  • optionally auto-inject tenant filters into query builders in ctx
  • reuse the same tenant-aware runtime for api.execute(...), api.run(...), and HTTP delivery

With mode: 'auto-inject', hypequery injects tenant filters into compatible query builders in context. That is the recommended mode for SaaS applications.

Global tenant configuration

import { initServe } from '@hypequery/serve';
import { z } from 'zod';

type AppAuth = { userId: string; tenantId: string };

const authStrategy = async ({ request }): Promise<AppAuth | null> => {
  const token = request.headers['authorization'];
  if (!token) return null;
  const decoded = await verifyToken(token);
  return { userId: decoded.sub, tenantId: decoded.organization_id };
};

const { query, serve } = initServe({
  auth: authStrategy,
  context: () => ({
    db: myDatabaseConnection,
  }),
});

const getOrders = query({
  input: z.object({ status: z.string().optional() }),
  query: ({ ctx, input }) =>
    ctx.db
      .table('orders')
      .where('status', 'eq', input.status ?? 'completed')
      .select('*')
      .execute(),
});

export const api = serve({
  tenant: {
    extract: (auth) => auth.tenantId,
    required: true,
    column: 'organization_id',
    mode: 'auto-inject',
  },
  queries: { getOrders },
});

Configuration options

extract

Use extract to read a tenant ID from your auth object.

column

Set the database column used for tenant filtering when you use mode: 'auto-inject'.

mode

  • auto-inject automatically applies tenant filters to compatible builders in context
  • manual leaves tenant filtering up to your query logic

required

When required is true, requests without a tenant ID are rejected.

Auto-inject mode

Auto-inject mode is the safest option because it makes tenant filtering the runtime default:

const { query, serve } = initServe({
  context: () => ({
    db: myDb,
    analyticsDb: myAnalyticsDb,
  }),
});

const getUsers = query({
  query: ({ ctx }) => ctx.db.table('users').select('*').execute(),
});

export const api = serve({
  tenant: {
    extract: (auth) => auth.tenantId,
    column: 'org_id',
    mode: 'auto-inject',
  },
  queries: { getUsers },
});

Manual mode

Use manual mode when you need to control tenant predicates yourself:

const { query, serve } = initServe({
  context: () => ({ db: myDb }),
});

const getUsers = query({
  query: ({ ctx }) =>
    ctx.db
      .table('users')
      .where('organization_id', 'eq', ctx.tenantId)
      .select('*')
      .execute(),
});

export const api = serve({
  tenant: {
    extract: (auth) => auth.tenantId,
    mode: 'manual',
  },
  queries: { getUsers },
});

Per-query tenant overrides

Object-style query({ ... }) supports per-query tenant overrides.

const { query, serve } = initServe({
  context: () => ({ db }),
});

const orders = query({
  query: ({ ctx }) => ctx.db.table('orders').select('*').execute(),
});

const adminStats = query({
  tenant: {
    required: false,
    mode: 'manual',
  },
  query: ({ ctx }) => {
    if (ctx.tenantId) {
      return ctx.db.table('stats').where('tenant_id', 'eq', ctx.tenantId).execute();
    }

    return ctx.db.table('stats').select('*').execute();
  },
});

export const api = serve({
  tenant: {
    extract: (auth) => auth.tenantId,
    column: 'tenant_id',
    mode: 'auto-inject',
  },
  queries: { orders, adminStats },
});

Use per-query overrides when one query needs different tenant behavior than the global runtime:

  • required: false makes tenant context optional for that query
  • mode: 'manual' disables auto-injection for that query
  • if no global tenant config exists, include extract in the per-query override

Standalone query.execute(...) does not run the tenant pipeline. Use api.execute(...), api.run(...), or an HTTP route when tenant enforcement matters.

See Also

On this page