Skip to content

Commit 08559b1

Browse files
authored
Merge pull request #1171 from objectstack-ai/claude/design-multitenant-architecture
feat(multi-tenant): Phase 2 - Turso Platform API integration and schema initialization
2 parents 6a0a20d + 9dbe1be commit 08559b1

25 files changed

+3183
-7
lines changed

ROADMAP.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,28 @@ Objects now declare `namespace: 'sys'` and a short `name` (e.g., `name: 'user'`)
606606
- [x] Metadata-driven deploy pipeline — `system/deploy-bundle.zod.ts`: `DeployBundleSchema`, `MigrationPlanSchema`, `DeployDiffSchema`; `contracts/deploy-pipeline-service.ts`: `IDeployPipelineService`
607607
- [x] App marketplace installation protocol — `system/app-install.zod.ts`: `AppManifestSchema`, `AppInstallResultSchema`, `AppCompatibilityCheckSchema`; `contracts/app-lifecycle-service.ts`: `IAppLifecycleService`
608608
- [ ] Cross-tenant data sharing policies
609+
- [x] **Phase 1: Multi-Tenant Protocol & Minimal Prototype (v3.4)** — ✅ Complete (2026-04-17)
610+
- [x] UUID-based tenant database naming (not org-slug, for immutability)
611+
- [x] Tenant registry schema — `cloud/tenant.zod.ts`: `TenantDatabaseSchema`, `PackageInstallationSchema`, `TenantContextSchema`, `TenantRoutingConfigSchema`, `ProvisionTenantRequestSchema`, `ProvisionTenantResponseSchema`
612+
- [x] `@objectstack/service-tenant` package — Tenant context service, multi-tenant router integration, UUID-based naming enforcement
613+
- [x] Tenant identification strategies — Subdomain, custom domain, HTTP header, JWT claim, session
614+
- [x] TenantContextService — Tenant context resolution with caching and multiple identification sources
615+
- [x] TenantProvisioningService skeleton — Minimal prototype for tenant database provisioning (Turso Platform API integration pending)
616+
- [x] Multi-tenant router documentation updates — UUID naming conventions and examples
617+
- [x] Test coverage — TenantContextService identification and caching tests
618+
- [x] **Phase 2: Turso Platform API Integration (v3.5)** — ✅ Complete (2026-04-17)
619+
- [x] Turso Platform API client implementation
620+
- [x] Automated tenant database creation
621+
- [x] Tenant-specific auth token generation
622+
- [x] Global control plane database setup (sys_tenant_registry, sys_package_installation)
623+
- [x] Tenant database schema initialization
624+
- [x] Package installation per tenant
625+
- [ ] **Phase 3: Production Hardening (v4.0)** — 🟡 Partially Complete
626+
- [x] Tenant lifecycle management (suspend, archive, restore)
627+
- [ ] Multi-region tenant migration
628+
- [ ] Tenant usage tracking and quota enforcement
629+
- [ ] Cross-tenant data sharing policies
630+
- [ ] Tenant-specific RBAC and permissions
609631

610632
### 6.3 Observability
611633

content/docs/references/cloud/index.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ This section contains all protocol schemas for the cloud layer of ObjectStack.
1010
<Card href="/docs/references/cloud/developer-portal" title="Developer Portal" description="Source: packages/spec/src/cloud/developer-portal.zod.ts" />
1111
<Card href="/docs/references/cloud/marketplace" title="Marketplace" description="Source: packages/spec/src/cloud/marketplace.zod.ts" />
1212
<Card href="/docs/references/cloud/marketplace-admin" title="Marketplace Admin" description="Source: packages/spec/src/cloud/marketplace-admin.zod.ts" />
13+
<Card href="/docs/references/cloud/tenant" title="Tenant" description="Source: packages/spec/src/cloud/tenant.zod.ts" />
1314
</Cards>

content/docs/references/cloud/meta.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"app-store",
55
"developer-portal",
66
"marketplace",
7-
"marketplace-admin"
7+
"marketplace-admin",
8+
"provisioning",
9+
"tenant"
810
]
911
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
title: Provisioning
3+
description: Provisioning protocol schemas
4+
---
5+
6+
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs go in content/docs/guides/. */}
7+
8+
<Callout type="info">
9+
**Source:** `packages/spec/src/cloud/provisioning.zod.ts`
10+
</Callout>
11+
12+
## TypeScript Usage
13+
14+
```typescript
15+
import { TenantPlan } from '@objectstack/spec/cloud';
16+
import type { TenantPlan } from '@objectstack/spec/cloud';
17+
18+
// Validate data
19+
const result = TenantPlan.parse(data);
20+
```
21+
22+
---
23+
24+
## TenantPlan
25+
26+
### Allowed Values
27+
28+
* `free`
29+
* `starter`
30+
* `pro`
31+
* `enterprise`
32+
* `custom`
33+
34+
35+
---
36+
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
---
2+
title: Tenant
3+
description: Tenant protocol schemas
4+
---
5+
6+
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs go in content/docs/guides/. */}
7+
8+
Multi-Tenant Architecture Schema
9+
10+
Defines the schema for managing multi-tenant architecture with:
11+
12+
- Global control plane: Single database for auth, org management, tenant registry
13+
14+
- Tenant data plane: Isolated databases per organization (UUID-based naming)
15+
16+
Design decisions:
17+
18+
- Database naming: \{uuid\}.turso.io (not org-slug, since slugs can be modified)
19+
20+
- Each tenant has its own Turso database for complete data isolation
21+
22+
- Global database stores user auth, organizations, and tenant metadata
23+
24+
<Callout type="info">
25+
**Source:** `packages/spec/src/cloud/tenant.zod.ts`
26+
</Callout>
27+
28+
## TypeScript Usage
29+
30+
```typescript
31+
import { PackageInstallation, PackageInstallationStatus, ProvisionTenantRequest, ProvisionTenantResponse, TenantContext, TenantDatabase, TenantDatabaseStatus, TenantIdentificationSource, TenantRoutingConfig } from '@objectstack/spec/cloud';
32+
import type { PackageInstallation, PackageInstallationStatus, ProvisionTenantRequest, ProvisionTenantResponse, TenantContext, TenantDatabase, TenantDatabaseStatus, TenantIdentificationSource, TenantRoutingConfig } from '@objectstack/spec/cloud';
33+
34+
// Validate data
35+
const result = PackageInstallation.parse(data);
36+
```
37+
38+
---
39+
40+
## PackageInstallation
41+
42+
### Properties
43+
44+
| Property | Type | Required | Description |
45+
| :--- | :--- | :--- | :--- |
46+
| **id** | `string` || Unique installation identifier |
47+
| **tenantId** | `string` || Tenant database ID |
48+
| **packageId** | `string` || Package identifier |
49+
| **version** | `string` || Installed package version |
50+
| **status** | `Enum<'installing' \| 'active' \| 'disabled' \| 'uninstalling' \| 'failed'>` || Installation status |
51+
| **installedAt** | `string` || Installation timestamp |
52+
| **installedBy** | `string` || User ID who installed the package |
53+
| **config** | `Record<string, any>` | optional | Package-specific configuration |
54+
| **updatedAt** | `string` || Last update timestamp |
55+
56+
57+
---
58+
59+
## PackageInstallationStatus
60+
61+
### Allowed Values
62+
63+
* `installing`
64+
* `active`
65+
* `disabled`
66+
* `uninstalling`
67+
* `failed`
68+
69+
70+
---
71+
72+
## ProvisionTenantRequest
73+
74+
### Properties
75+
76+
| Property | Type | Required | Description |
77+
| :--- | :--- | :--- | :--- |
78+
| **organizationId** | `string` || Organization ID |
79+
| **region** | `string` | optional | Deployment region preference |
80+
| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` || Tenant plan tier |
81+
| **storageLimitMb** | `integer` | optional | Storage limit in megabytes |
82+
| **metadata** | `Record<string, any>` | optional | Custom tenant metadata |
83+
84+
85+
---
86+
87+
## ProvisionTenantResponse
88+
89+
### Properties
90+
91+
| Property | Type | Required | Description |
92+
| :--- | :--- | :--- | :--- |
93+
| **tenant** | `Object` || Provisioned tenant database |
94+
| **durationMs** | `number` || Provisioning duration in milliseconds |
95+
| **warnings** | `string[]` | optional | Provisioning warnings |
96+
97+
98+
---
99+
100+
## TenantContext
101+
102+
### Properties
103+
104+
| Property | Type | Required | Description |
105+
| :--- | :--- | :--- | :--- |
106+
| **tenantId** | `string` || Current tenant database ID |
107+
| **organizationId** | `string` || Current organization ID |
108+
| **organizationSlug** | `string` | optional | Organization slug |
109+
| **databaseUrl** | `string` || Tenant database URL |
110+
| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` || Tenant plan tier |
111+
| **metadata** | `Record<string, any>` | optional | Custom tenant metadata |
112+
113+
114+
---
115+
116+
## TenantDatabase
117+
118+
### Properties
119+
120+
| Property | Type | Required | Description |
121+
| :--- | :--- | :--- | :--- |
122+
| **id** | `string` || Unique tenant database identifier (UUID) |
123+
| **organizationId** | `string` || Organization ID (foreign key to sys_organization) |
124+
| **databaseName** | `string` || Database name (UUID-based) |
125+
| **databaseUrl** | `string` || Full database URL |
126+
| **authToken** | `string` || Encrypted tenant-specific auth token |
127+
| **status** | `Enum<'provisioning' \| 'active' \| 'suspended' \| 'archived' \| 'failed'>` || Database status |
128+
| **region** | `string` || Deployment region |
129+
| **plan** | `Enum<'free' \| 'starter' \| 'pro' \| 'enterprise' \| 'custom'>` || Tenant plan tier |
130+
| **storageLimitMb** | `integer` || Storage limit in megabytes |
131+
| **createdAt** | `string` || Database creation timestamp |
132+
| **updatedAt** | `string` || Last update timestamp |
133+
| **lastAccessedAt** | `string` | optional | Last accessed timestamp |
134+
| **metadata** | `Record<string, any>` | optional | Custom tenant configuration |
135+
136+
137+
---
138+
139+
## TenantDatabaseStatus
140+
141+
### Allowed Values
142+
143+
* `provisioning`
144+
* `active`
145+
* `suspended`
146+
* `archived`
147+
* `failed`
148+
149+
150+
---
151+
152+
## TenantIdentificationSource
153+
154+
### Allowed Values
155+
156+
* `subdomain`
157+
* `custom_domain`
158+
* `header`
159+
* `jwt_claim`
160+
* `session`
161+
* `default`
162+
163+
164+
---
165+
166+
## TenantRoutingConfig
167+
168+
### Properties
169+
170+
| Property | Type | Required | Description |
171+
| :--- | :--- | :--- | :--- |
172+
| **enabled** | `boolean` || Enable multi-tenant mode |
173+
| **identificationSources** | `Enum<'subdomain' \| 'custom_domain' \| 'header' \| 'jwt_claim' \| 'session' \| 'default'>[]` || Tenant identification strategy (in order of precedence) |
174+
| **defaultTenantId** | `string` | optional | Default tenant ID |
175+
| **subdomainPattern** | `string` | optional | Subdomain pattern for tenant extraction |
176+
| **customDomainMapping** | `Record<string, string>` | optional | Custom domain to tenant ID mapping |
177+
| **tenantHeaderName** | `string` || Header name for tenant ID |
178+
| **jwtOrganizationClaim** | `string` || JWT claim name for organization ID |
179+
180+
181+
---
182+

packages/plugins/driver-turso/src/multi-tenant.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
*
66
* Manages per-tenant TursoDriver instances with TTL-based caching.
77
* Uses a URL template with `{tenant}` placeholder that is replaced
8-
* with the tenantId at runtime.
8+
* with the tenantId (UUID) at runtime.
99
*
10-
* Serverless-safe: no global intervals, no leaked state. Expired
10+
* **UUID-Based Tenant Naming:**
11+
* - Tenant IDs are UUIDs, not organization slugs
12+
* - Ensures database URLs remain stable even if organization names change
13+
* - Example: `550e8400-e29b-41d4-a716-446655440000.turso.io`
14+
*
15+
* **Serverless-safe:** no global intervals, no leaked state. Expired
1116
* entries are evicted lazily on next access.
1217
*/
1318

@@ -18,20 +23,32 @@ import { TursoDriver, type TursoDriverConfig } from './turso-driver.js';
1823
/**
1924
* Configuration for the multi-tenant router.
2025
*
26+
* **UUID-Based Tenant Naming:**
27+
* The `{tenant}` placeholder is replaced with a UUID, not an organization slug.
28+
* This ensures database URLs remain stable even if organization names change.
29+
*
2130
* @example
2231
* ```typescript
2332
* const router = createMultiTenantRouter({
24-
* urlTemplate: 'file:./data/{tenant}.db',
33+
* // UUID-based URL template
34+
* urlTemplate: 'libsql://{tenant}.turso.io',
35+
* groupAuthToken: process.env.TURSO_GROUP_TOKEN,
2536
* clientCacheTTL: 300_000, // 5 minutes
2637
* });
2738
*
28-
* const driver = await router.getDriverForTenant('acme');
39+
* // Tenant ID is a UUID
40+
* const driver = await router.getDriverForTenant('550e8400-e29b-41d4-a716-446655440000');
2941
* ```
3042
*/
3143
export interface MultiTenantConfig {
3244
/**
33-
* URL template with `{tenant}` placeholder.
34-
* Example: `'file:./data/{tenant}.db'`
45+
* URL template with `{tenant}` placeholder (replaced with UUID at runtime).
46+
*
47+
* Examples:
48+
* - Remote: `'libsql://{tenant}.turso.io'`
49+
* - Local: `'file:./data/{tenant}.db'`
50+
*
51+
* **Important:** Use UUID for tenant ID, not organization slug.
3552
*/
3653
urlTemplate: string;
3754

0 commit comments

Comments
 (0)