Skip to content

Commit 07e9be5

Browse files
committed
feat(docs): add project-scoping guide and update system architecture implementation summary
refactor(tests): rename environment-related tests to project-related and remove obsolete test file fix(tests): update environment object tests to reflect project object changes
1 parent 51eb6fc commit 07e9be5

5 files changed

Lines changed: 212 additions & 273 deletions

File tree

content/docs/guides/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"---Integration---",
1919
"api-reference",
2020
"client-sdk",
21+
"project-scoping",
2122
"driver-configuration",
2223
"kernel-services",
2324
"data-flow",
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
title: Project-Scoped Routing
3+
description: Enable multi-project isolation at the URL level — /api/v1/projects/:projectId/... — with REST, Client SDK, and dispatcher support.
4+
---
5+
6+
# Project-Scoped Routing
7+
8+
ObjectStack's project-scoping feature puts each project (the "workspace" or "base" in Airtable / "environment" in traditional stacks) into its own URL segment. When enabled, every data, metadata, automation, and AI request runs through `/api/v1/projects/:projectId/...` instead of relying on hostname / header / session resolution alone.
9+
10+
This gives you:
11+
12+
- **Explicit tenancy in URLs** — no more guessing what project a request targets.
13+
- **Safer client code**`client.project(id).data.find(...)` cannot be confused with the default project.
14+
- **Future foundation for RBAC** — the project is in the URL, so middleware can enforce access before any handler runs.
15+
- **Incremental rollout** — the `'auto'` resolution strategy mounts both scoped and unscoped routes, so you can migrate clients on your own schedule.
16+
17+
## Server configuration
18+
19+
Enable scoping in your `objectstack.config.ts`:
20+
21+
```typescript
22+
import { defineStack } from '@objectstack/spec';
23+
24+
export default defineStack({
25+
// ... rest of your stack
26+
api: {
27+
enableProjectScoping: true,
28+
projectResolution: 'auto', // 'required' | 'optional' | 'auto'
29+
},
30+
});
31+
```
32+
33+
### Resolution strategies
34+
35+
| Strategy | Behavior | When to use |
36+
| ------------ | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
37+
| `'auto'` | Registers **both** `/api/v1/data/:object` and `/api/v1/projects/:projectId/data/:object`. | Default. Zero risk during migration — old and new clients both work. |
38+
| `'optional'` | Same as `auto` today; reserved so a future release can require one of URL / header / session to be present. | Same as `auto`. |
39+
| `'required'` | Only scoped routes are registered. Unscoped requests return 404. | Production hardening after all clients have migrated. |
40+
41+
The setting also propagates to:
42+
43+
- **Dispatcher plugin** — AI (`/ai/*`) and automation (`/automation/*`) routes gain scoped variants.
44+
- **Package management**`/api/v1/projects/:projectId/packages` mirrors `/api/v1/packages`.
45+
- **Discovery** — the `/api/v1/discovery` response includes a `scoping` block so clients can detect the mode.
46+
47+
## Client SDK
48+
49+
The client exposes a `project(id)` factory that returns a sub-client wired to the scoped URL prefix:
50+
51+
```typescript
52+
import { ObjectStackClient } from '@objectstack/client';
53+
54+
const client = new ObjectStackClient({ baseUrl: 'https://api.example.com' });
55+
56+
const scoped = client.project('00000000-0000-0000-0000-000000000001');
57+
58+
// All calls hit /api/v1/projects/:projectId/...
59+
const tasks = await scoped.data.find('task', { top: 20 });
60+
const objects = await scoped.meta.getItems('object');
61+
const pkg = await scoped.packages.get('my-package');
62+
```
63+
64+
The top-level `client.data.*`, `client.meta.*`, `client.packages.*` namespaces continue to work unchanged — they hit the unscoped routes, falling back to hostname / `X-Project-Id` header / session resolution exactly as before.
65+
66+
You can switch between projects without rebuilding the client:
67+
68+
```typescript
69+
const projectA = client.project('proj-a');
70+
const projectB = client.project('proj-b');
71+
72+
await Promise.all([
73+
projectA.data.find('task'),
74+
projectB.data.find('task'),
75+
]);
76+
```
77+
78+
## Request resolution order
79+
80+
When `enableProjectScoping: true`, `HttpDispatcher.resolveEnvironmentContext` resolves the project in this order:
81+
82+
1. **URL path parameter**`/api/v1/projects/:projectId/...`. Highest priority.
83+
2. **Hostname**`acme-dev.objectstack.app`.
84+
3. **`X-Project-Id` header** — the client SDK sets this automatically if you pass `projectId` in `ClientConfig`.
85+
4. **Session**`session.activeProjectId` (or legacy `activeEnvironmentId`).
86+
5. **Default project for active organization** — falls back to the org's default.
87+
88+
Under `'required'` mode, the URL path is the only supported source; the other fallbacks are bypassed for data/meta/AI/automation routes.
89+
90+
## System project
91+
92+
Every deployment has a built-in system project at the well-known UUID `00000000-0000-0000-0000-000000000001`. It's used for platform infrastructure (system packages, platform-level flows, etc.) and is created automatically on first startup if you register the plugin:
93+
94+
```typescript
95+
import { createSystemProjectPlugin } from '@objectstack/runtime';
96+
97+
kernel.use(tenantPlugin);
98+
kernel.use(createSystemProjectPlugin());
99+
```
100+
101+
The plugin is idempotent — it's safe to call `provisionSystemProject()` on every boot. Subsequent calls return the existing row.
102+
103+
## Control-plane routes remain unscoped
104+
105+
Control-plane endpoints always live under their unscoped paths:
106+
107+
- `/api/v1/auth/*`
108+
- `/api/v1/cloud/projects` (CRUD on the project list itself)
109+
- `/api/v1/cloud/organizations`
110+
- `/api/v1/health`
111+
- `/api/v1/discovery`
112+
- `/.well-known/objectstack`
113+
114+
These are not affected by `enableProjectScoping` because they operate on the control plane, not a specific project's data plane.
115+
116+
## Migration checklist
117+
118+
1. ✅ Upgrade to `@objectstack/core@≥4.x` with the Phase 2 runtime.
119+
2. ✅ Add `enableProjectScoping: true, projectResolution: 'auto'` to your stack config.
120+
3. ✅ Register `createSystemProjectPlugin()` in your kernel wiring.
121+
4. ⏳ Update callers that hard-code `/api/v1/data/...` to use `client.project(id)` or keep them as-is under `auto` mode.
122+
5. ⏳ Once all callers have migrated, flip `projectResolution` to `'required'` and remove the unscoped code paths.
123+
124+
Steps 4 and 5 are intentionally separate — you can ship Step 3 today and migrate clients over weeks or months.
125+
126+
## Related reading
127+
128+
- [ADR-0002 — Project-per-database isolation](/docs/adr/0002-environment-database-isolation)
129+
- [Client SDK](/docs/guides/client-sdk)
130+
- [Authentication](/docs/guides/authentication)

docs/design/SYSTEM_ARCHITECTURE_IMPLEMENTATION.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,50 @@
11
# System Architecture Implementation Summary
22

3-
**Status**: Phase 1 Complete ✅
3+
**Status**: Phase 1 & Phase 2 Complete ✅
44
**Date**: 2026-04-22
55
**Branch**: `claude/design-new-system-architecture`
66

7-
## Overview
7+
---
8+
9+
## Phase 2 — Runtime Implementation (2026-04-22)
10+
11+
Phase 2 wires the Phase 1 schemas into the runtime so project-scoped APIs actually work end-to-end.
12+
13+
### What was delivered
14+
15+
1. **REST server dual-mode routing**`RestServer.registerRoutes()` now reads `enableProjectScoping` / `projectResolution` from the normalized config and registers CRUD / metadata / batch / UI / package / discovery handlers under both `/api/v1/...` and `/api/v1/projects/:projectId/...` (or only the scoped form in `'required'` mode). Scoped handlers forward `req.params.projectId` into every protocol call.
16+
2. **HttpDispatcher URL-param resolution** — new `extractProjectIdFromPath` helper and a top-of-chain branch in `resolveEnvironmentContext` that resolves a project from the URL path before falling back to hostname / header / session. `/cloud/projects/...` is explicitly excluded so it does not collide with the scoping pattern.
17+
3. **Dispatcher plugin scoping** — automation and AI routes now mount both unscoped and scoped variants when `DispatcherPluginConfig.scoping.enableProjectScoping` is set. Routes share a single handler factory.
18+
4. **Client SDK `project(id)` factory** — new `ScopedProjectClient` class in `packages/client/src/index.ts` exposes `data`, `meta`, and `packages` namespaces under the scoped URL prefix. `client.data.*` / `client.meta.*` remain untouched for backward compatibility.
19+
5. **System project bootstrap plugin** — new `createSystemProjectPlugin()` in `@objectstack/runtime` idempotently calls `ProjectProvisioningService.provisionSystemProject()` on startup and logs the well-known UUID. Strict mode throws on failure; default mode logs a warning and continues.
20+
6. **Tenant object rename follow-up** — stale `sys_environment*` tests were rewritten to target the renamed `sys_project*` objects. The obsolete `environment-provisioning.test.ts` (duplicate of `project-provisioning.test.ts`) was deleted.
21+
7. **Documentation** — new guide `content/docs/guides/project-scoping.mdx` walks developers through server config, client usage, resolution order, and the `auto``required` migration path.
22+
23+
### Test coverage added in Phase 2
24+
25+
- `packages/rest/src/rest.test.ts` — +5 tests covering default / `auto` / `required` registration and projectId propagation into protocol calls.
26+
- `packages/runtime/src/http-dispatcher.test.ts` — +3 tests covering URL-param precedence, `/cloud/projects/` skip, and fallback to header resolution.
27+
- `packages/runtime/src/system-project-plugin.test.ts` (new) — +7 tests covering service resolution, strict vs lenient failure handling, and idempotent invocation.
28+
- `packages/client/src/client.test.ts` — +6 tests covering URL prefixing, encoding, validation, and accessors on `ScopedProjectClient`.
29+
- `packages/client/src/client.project-scoping.test.ts` (new) — live Hono integration test: 5 cases covering scoped CRUD, unscoped CRUD backward compat, scoped meta, end-to-end `client.project(id).data.find()`, and discovery `scoping` metadata.
30+
- `packages/services/service-tenant/src/objects/environment-objects.test.ts` — rewritten to target the renamed `sys_project*` objects; 17 passing tests.
31+
32+
**Aggregate**: 305 tests passing across `@objectstack/rest` (46), `@objectstack/runtime` (160), `@objectstack/client` (99); `@objectstack/service-tenant` went from 7 failed / 30 passed to 38 passed / 2 skipped.
33+
34+
### What remains for Phase 3 and beyond
35+
36+
Each item below is intentionally sized to be a focused PR:
37+
38+
- **Studio UI migration** — Studio pages should call `client.project(id).*` once a project switcher is wired in. Large surface under `apps/studio/`.
39+
- **CLI `projects` commands**`objectstack projects list/create/switch` in `packages/cli/src/commands/projects/`.
40+
- **Browser E2E** — Playwright suite covering login → project switch → data/meta flows.
41+
- **`projectResolution: 'required'` as default** — requires Studio migration and examples update; flip only after a deprecation cycle.
42+
- **Project-level RBAC middleware** — formal hook that enforces `can(user, 'read:project', projectId)` before any scoped handler runs. `DefaultEnvironmentDriverRegistry` already caches drivers, so this only needs an auth check layer.
43+
- **`envRegistry` rename** — internal field name in `HttpDispatcher` still uses the environment-era naming; cosmetic rename deferred to avoid touching constructors across the runtime test suite.
44+
45+
---
46+
47+
## Phase 1 Overview
848

949
This document summarizes the implementation of ObjectStack's new system architecture featuring a built-in "system" project and project-scoped API routing configuration, following Airtable's workspace/base scoping model.
1050

0 commit comments

Comments
 (0)