@usebetterdev/tenant 0.1.0 → 0.2.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # @usebetterdev/tenant
2
+
3
+ Multi-tenancy for Postgres in minutes. Database-enforced tenant isolation via Row-Level Security, with adapters for Hono, Express, and Next.js.
4
+
5
+ - **RLS-based isolation** — tenant boundaries enforced at the database level, not application code
6
+ - **Framework adapters** — Hono, Express, Next.js App Router
7
+ - **Drizzle ORM** — first-class adapter with transaction-scoped tenant context
8
+ - **CLI** — generate migrations, verify setup, seed tenants
9
+ - **Zero WHERE clauses** — queries are automatically scoped to the current tenant
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @usebetterdev/tenant
15
+ # CLI (dev dependency)
16
+ npm install -D @usebetterdev/tenant-cli
17
+ ```
18
+
19
+ ## Quick start
20
+
21
+ ### 1. Initialize config
22
+
23
+ ```bash
24
+ npx @usebetterdev/tenant-cli init --database-url $DATABASE_URL
25
+ ```
26
+
27
+ This detects your tables and creates `better-tenant.config.json` interactively. Or create it manually:
28
+
29
+ ```json
30
+ {
31
+ "tenantTables": ["projects", "tasks"]
32
+ }
33
+ ```
34
+
35
+ ### 2. Generate and apply migration
36
+
37
+ ```bash
38
+ npx @usebetterdev/tenant-cli migrate -o ./migrations
39
+ psql $DATABASE_URL -f ./migrations/*_better_tenant.sql
40
+ ```
41
+
42
+ This creates the `tenants` table, RLS policies, and triggers for each table listed in `tenantTables`.
43
+
44
+ ### 3. Verify setup
45
+
46
+ ```bash
47
+ npx @usebetterdev/tenant-cli check --database-url $DATABASE_URL
48
+ ```
49
+
50
+ ### 4. Seed a tenant
51
+
52
+ ```bash
53
+ npx @usebetterdev/tenant-cli seed --name "Acme Corp" --database-url $DATABASE_URL
54
+ ```
55
+
56
+ ### 5. Wire up your app
57
+
58
+ ```ts
59
+ import { drizzle } from "drizzle-orm/node-postgres";
60
+ import { Pool } from "pg";
61
+ import { betterTenant } from "@usebetterdev/tenant";
62
+ import {
63
+ drizzleAdapter,
64
+ createGetTenantRepository,
65
+ tenantsTable,
66
+ } from "@usebetterdev/tenant/drizzle";
67
+
68
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
69
+ const db = drizzle(pool);
70
+
71
+ export const tenant = betterTenant({
72
+ adapter: drizzleAdapter(db),
73
+ tenantResolver: { header: "x-tenant-id" },
74
+ getTenantRepository: createGetTenantRepository(tenantsTable),
75
+ });
76
+ ```
77
+
78
+ ### 6. Add framework middleware
79
+
80
+ **Hono**
81
+
82
+ ```ts
83
+ import { createHonoMiddleware } from "@usebetterdev/tenant/hono";
84
+
85
+ app.use("*", createHonoMiddleware(tenant));
86
+ ```
87
+
88
+ **Express**
89
+
90
+ ```ts
91
+ import { createExpressMiddleware } from "@usebetterdev/tenant/express";
92
+
93
+ app.use(createExpressMiddleware(tenant));
94
+ ```
95
+
96
+ **Next.js App Router**
97
+
98
+ ```ts
99
+ import { withTenant } from "@usebetterdev/tenant/next";
100
+
101
+ export const GET = withTenant(tenant, async (request) => {
102
+ // tenant context is available here
103
+ return Response.json({ ok: true });
104
+ });
105
+ ```
106
+
107
+ ### 7. Use in handlers
108
+
109
+ ```ts
110
+ // Current tenant
111
+ const ctx = tenant.getContext();
112
+ ctx.tenantId; // "550e8400-..."
113
+ ctx.tenant; // { id, name, slug, createdAt }
114
+
115
+ // Tenant-scoped database (all queries filtered by RLS)
116
+ const db = tenant.getDatabase();
117
+ const projects = await db.select().from(projectsTable);
118
+ // ^ returns only current tenant's projects — no WHERE needed
119
+ ```
120
+
121
+ ## Subpath exports
122
+
123
+ | Import | Description |
124
+ | ------------------------------ | ------------------------------------------------------------------------------ |
125
+ | `@usebetterdev/tenant` | Core API: `betterTenant`, `getContext`, `runAs`, `runAsSystem` |
126
+ | `@usebetterdev/tenant/drizzle` | Drizzle adapter: `drizzleAdapter`, `tenantsTable`, `createGetTenantRepository` |
127
+ | `@usebetterdev/tenant/hono` | Hono middleware: `createHonoMiddleware` |
128
+ | `@usebetterdev/tenant/express` | Express middleware: `createExpressMiddleware` |
129
+ | `@usebetterdev/tenant/next` | Next.js wrapper: `withTenant` |
130
+
131
+ ## Tenant resolver
132
+
133
+ Configure how the tenant ID is extracted from incoming requests. Resolution order: **header > path > subdomain > JWT > custom**.
134
+
135
+ ```ts
136
+ tenantResolver: {
137
+ header: "x-tenant-id", // from request header
138
+ path: "/t/:tenantId/*", // from URL path segment
139
+ subdomain: true, // from subdomain (acme.app.com)
140
+ jwt: { claim: "tenant_id" }, // from JWT payload
141
+ custom: (req) => extractTenant(req), // custom function
142
+ }
143
+ ```
144
+
145
+ ## Tenant API
146
+
147
+ When `getTenantRepository` is configured, CRUD operations on the `tenants` table are available via `tenant.tenant.api`. The adapter must also support `runAsSystem` (e.g. Drizzle adapter); all API calls run with RLS bypass so they can read and write tenants across the system. **Restrict these endpoints to admins** — do not expose them to regular tenant users.
148
+
149
+ | Method | Description |
150
+ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
151
+ | **createTenant(data)** | Create a tenant. `data`: `{ name: string, slug: string }` (both required, trimmed). Returns the created `Tenant` (`id`, `name`, `slug`, `createdAt`). |
152
+ | **listTenants(options?)** | List tenants with pagination. `options`: `{ limit?: number, offset?: number }`. Default `limit` 50, max 50. Returns `Tenant[]`. |
153
+ | **updateTenant(tenantId, data)** | Update a tenant by ID. `data`: `{ name?: string, slug?: string }`. Returns the updated `Tenant`. |
154
+ | **deleteTenant(tenantId)** | Delete a tenant by ID. Returns `void`. |
155
+
156
+ Example:
157
+
158
+ ```ts
159
+ // Create
160
+ const created = await tenant.tenant.api.createTenant({
161
+ name: "Acme Corp",
162
+ slug: "acme",
163
+ });
164
+
165
+ // List (e.g. admin dashboard)
166
+ const tenants = await tenant.tenant.api.listTenants({ limit: 20, offset: 0 });
167
+
168
+ // Update
169
+ await tenant.tenant.api.updateTenant(created.id, {
170
+ name: "Acme Inc",
171
+ slug: "acme-inc",
172
+ });
173
+
174
+ // Delete
175
+ await tenant.tenant.api.deleteTenant(created.id);
176
+ ```
177
+
178
+ ## Admin operations
179
+
180
+ ```ts
181
+ import { runAs, runAsSystem } from "@usebetterdev/tenant";
182
+
183
+ // Run as a specific tenant (cron jobs, background tasks)
184
+ await tenant.runAs(tenantId, adapter, async (db) => {
185
+ await db.select().from(projectsTable); // scoped to tenant
186
+ });
187
+
188
+ // Run with RLS bypass (admin, cross-tenant reporting)
189
+ await tenant.runAsSystem(async (db) => {
190
+ await db.select().from(projectsTable); // all tenants
191
+ });
192
+ ```
193
+
194
+ ## How it works
195
+
196
+ 1. Request arrives with tenant identifier (header, path, subdomain, JWT, or custom)
197
+ 2. Middleware resolves the tenant ID and starts a database transaction
198
+ 3. `SET LOCAL app.current_tenant = '<uuid>'` scopes all queries via RLS
199
+ 4. Your handler runs — every query is automatically filtered to the current tenant
200
+ 5. Transaction commits, `SET LOCAL` auto-clears (safe for connection pooling)
201
+
202
+ ## Telemetry
203
+
204
+ Anonymous telemetry is on by default. Opt out with `BETTER_TENANT_TELEMETRY=0` or `telemetry: { enabled: false }` in config.
205
+
206
+ ## License
207
+
208
+ MIT
package/dist/index.cjs CHANGED
@@ -25,7 +25,6 @@ __export(index_exports, {
25
25
  betterTenant: () => import_tenant_core.betterTenant,
26
26
  createTenantApi: () => import_tenant_core.createTenantApi,
27
27
  getContext: () => import_tenant_core.getContext,
28
- getDatabase: () => import_tenant_core.getDatabase,
29
28
  handleRequest: () => import_tenant_core.handleRequest,
30
29
  resolveTenant: () => import_tenant_core.resolveTenant,
31
30
  resolveTenantAsync: () => import_tenant_core.resolveTenantAsync,
@@ -44,7 +43,6 @@ var import_tenant_core = require("@usebetterdev/tenant-core");
44
43
  betterTenant,
45
44
  createTenantApi,
46
45
  getContext,
47
- getDatabase,
48
46
  handleRequest,
49
47
  resolveTenant,
50
48
  resolveTenantAsync,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export {\n betterTenant,\n getContext,\n getDatabase,\n runWithTenant,\n runWithTenantAndDatabase,\n resolveTenant,\n resolveTenantAsync,\n handleRequest,\n toResolvableRequest,\n runAs,\n runAsSystem,\n createTenantApi,\n TenantNotResolvedError,\n TenantMiddlewareError,\n} from \"@usebetterdev/tenant-core\";\nexport type {\n BetterTenantInstance,\n TenantApi,\n CreateTenantData,\n UpdateTenantData,\n ListTenantsOptions,\n HandleRequestOptions,\n Tenant,\n TenantContext,\n TenantScopedDatabase,\n SystemDatabase,\n TenantAdapter,\n TenantRepository,\n TenantResolverConfig,\n ResolvableRequest,\n BetterTenantPlugin,\n BetterTenantConfig,\n NodeLikeRequest,\n PathResolverConfig,\n SubdomainResolverConfig,\n JwtResolverConfig,\n} from \"@usebetterdev/tenant-core\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAeO;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export {\n betterTenant,\n getContext,\n runWithTenant,\n runWithTenantAndDatabase,\n resolveTenant,\n resolveTenantAsync,\n handleRequest,\n toResolvableRequest,\n runAs,\n runAsSystem,\n createTenantApi,\n TenantNotResolvedError,\n TenantMiddlewareError,\n} from \"@usebetterdev/tenant-core\";\nexport type {\n BetterTenantInstance,\n TenantApi,\n CreateTenantData,\n UpdateTenantData,\n ListTenantsOptions,\n HandleRequestOptions,\n Tenant,\n TenantContext,\n TenantScopedDatabase,\n SystemDatabase,\n TenantAdapter,\n TenantRepository,\n TenantResolverConfig,\n ResolvableRequest,\n BetterTenantPlugin,\n BetterTenantConfig,\n NodeLikeRequest,\n PathResolverConfig,\n SubdomainResolverConfig,\n JwtResolverConfig,\n} from \"@usebetterdev/tenant-core\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAcO;","names":[]}
package/dist/index.d.cts CHANGED
@@ -1 +1 @@
1
- export { BetterTenantConfig, BetterTenantInstance, BetterTenantPlugin, CreateTenantData, HandleRequestOptions, JwtResolverConfig, ListTenantsOptions, NodeLikeRequest, PathResolverConfig, ResolvableRequest, SubdomainResolverConfig, SystemDatabase, Tenant, TenantAdapter, TenantApi, TenantContext, TenantMiddlewareError, TenantNotResolvedError, TenantRepository, TenantResolverConfig, TenantScopedDatabase, UpdateTenantData, betterTenant, createTenantApi, getContext, getDatabase, handleRequest, resolveTenant, resolveTenantAsync, runAs, runAsSystem, runWithTenant, runWithTenantAndDatabase, toResolvableRequest } from '@usebetterdev/tenant-core';
1
+ export { BetterTenantConfig, BetterTenantInstance, BetterTenantPlugin, CreateTenantData, HandleRequestOptions, JwtResolverConfig, ListTenantsOptions, NodeLikeRequest, PathResolverConfig, ResolvableRequest, SubdomainResolverConfig, SystemDatabase, Tenant, TenantAdapter, TenantApi, TenantContext, TenantMiddlewareError, TenantNotResolvedError, TenantRepository, TenantResolverConfig, TenantScopedDatabase, UpdateTenantData, betterTenant, createTenantApi, getContext, handleRequest, resolveTenant, resolveTenantAsync, runAs, runAsSystem, runWithTenant, runWithTenantAndDatabase, toResolvableRequest } from '@usebetterdev/tenant-core';
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { BetterTenantConfig, BetterTenantInstance, BetterTenantPlugin, CreateTenantData, HandleRequestOptions, JwtResolverConfig, ListTenantsOptions, NodeLikeRequest, PathResolverConfig, ResolvableRequest, SubdomainResolverConfig, SystemDatabase, Tenant, TenantAdapter, TenantApi, TenantContext, TenantMiddlewareError, TenantNotResolvedError, TenantRepository, TenantResolverConfig, TenantScopedDatabase, UpdateTenantData, betterTenant, createTenantApi, getContext, getDatabase, handleRequest, resolveTenant, resolveTenantAsync, runAs, runAsSystem, runWithTenant, runWithTenantAndDatabase, toResolvableRequest } from '@usebetterdev/tenant-core';
1
+ export { BetterTenantConfig, BetterTenantInstance, BetterTenantPlugin, CreateTenantData, HandleRequestOptions, JwtResolverConfig, ListTenantsOptions, NodeLikeRequest, PathResolverConfig, ResolvableRequest, SubdomainResolverConfig, SystemDatabase, Tenant, TenantAdapter, TenantApi, TenantContext, TenantMiddlewareError, TenantNotResolvedError, TenantRepository, TenantResolverConfig, TenantScopedDatabase, UpdateTenantData, betterTenant, createTenantApi, getContext, handleRequest, resolveTenant, resolveTenantAsync, runAs, runAsSystem, runWithTenant, runWithTenantAndDatabase, toResolvableRequest } from '@usebetterdev/tenant-core';
package/dist/index.js CHANGED
@@ -2,7 +2,6 @@
2
2
  import {
3
3
  betterTenant,
4
4
  getContext,
5
- getDatabase,
6
5
  runWithTenant,
7
6
  runWithTenantAndDatabase,
8
7
  resolveTenant,
@@ -21,7 +20,6 @@ export {
21
20
  betterTenant,
22
21
  createTenantApi,
23
22
  getContext,
24
- getDatabase,
25
23
  handleRequest,
26
24
  resolveTenant,
27
25
  resolveTenantAsync,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export {\n betterTenant,\n getContext,\n getDatabase,\n runWithTenant,\n runWithTenantAndDatabase,\n resolveTenant,\n resolveTenantAsync,\n handleRequest,\n toResolvableRequest,\n runAs,\n runAsSystem,\n createTenantApi,\n TenantNotResolvedError,\n TenantMiddlewareError,\n} from \"@usebetterdev/tenant-core\";\nexport type {\n BetterTenantInstance,\n TenantApi,\n CreateTenantData,\n UpdateTenantData,\n ListTenantsOptions,\n HandleRequestOptions,\n Tenant,\n TenantContext,\n TenantScopedDatabase,\n SystemDatabase,\n TenantAdapter,\n TenantRepository,\n TenantResolverConfig,\n ResolvableRequest,\n BetterTenantPlugin,\n BetterTenantConfig,\n NodeLikeRequest,\n PathResolverConfig,\n SubdomainResolverConfig,\n JwtResolverConfig,\n} from \"@usebetterdev/tenant-core\";\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export {\n betterTenant,\n getContext,\n runWithTenant,\n runWithTenantAndDatabase,\n resolveTenant,\n resolveTenantAsync,\n handleRequest,\n toResolvableRequest,\n runAs,\n runAsSystem,\n createTenantApi,\n TenantNotResolvedError,\n TenantMiddlewareError,\n} from \"@usebetterdev/tenant-core\";\nexport type {\n BetterTenantInstance,\n TenantApi,\n CreateTenantData,\n UpdateTenantData,\n ListTenantsOptions,\n HandleRequestOptions,\n Tenant,\n TenantContext,\n TenantScopedDatabase,\n SystemDatabase,\n TenantAdapter,\n TenantRepository,\n TenantResolverConfig,\n ResolvableRequest,\n BetterTenantPlugin,\n BetterTenantConfig,\n NodeLikeRequest,\n PathResolverConfig,\n SubdomainResolverConfig,\n JwtResolverConfig,\n} from \"@usebetterdev/tenant-core\";\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,20 @@
1
1
  {
2
2
  "name": "@usebetterdev/tenant",
3
- "version": "0.1.0",
3
+ "description": "Multi-tenancy for Postgres in minutes. RLS-based tenant isolation, framework adapters (Hono, Express, Next.js), Drizzle ORM support.",
4
+ "version": "0.2.0-beta.9",
5
+ "keywords": [
6
+ "multi-tenancy",
7
+ "postgres",
8
+ "rls",
9
+ "row-level-security",
10
+ "tenant",
11
+ "drizzle",
12
+ "hono",
13
+ "express",
14
+ "nextjs",
15
+ "saas"
16
+ ],
17
+ "license": "MIT",
4
18
  "repository": "github:usebetter-dev/usebetter",
5
19
  "bugs": "https://github.com/usebetter-dev/usebetter/issues",
6
20
  "homepage": "https://github.com/usebetter-dev/usebetter#readme",
@@ -42,14 +56,15 @@
42
56
  }
43
57
  },
44
58
  "files": [
45
- "dist"
59
+ "dist",
60
+ "README.md"
46
61
  ],
47
62
  "dependencies": {
48
- "@usebetterdev/tenant-express": "0.1.0",
49
- "@usebetterdev/tenant-core": "0.1.0",
50
- "@usebetterdev/tenant-hono": "0.1.0",
51
- "@usebetterdev/tenant-drizzle": "0.1.0",
52
- "@usebetterdev/tenant-next": "0.1.0"
63
+ "@usebetterdev/tenant-core": "0.2.0-beta.9",
64
+ "@usebetterdev/tenant-next": "0.2.0-beta.9",
65
+ "@usebetterdev/tenant-express": "0.2.0-beta.9",
66
+ "@usebetterdev/tenant-hono": "0.2.0-beta.9",
67
+ "@usebetterdev/tenant-drizzle": "0.2.0-beta.9"
53
68
  },
54
69
  "devDependencies": {
55
70
  "tsup": "^8.3.5",