OpenChamber Rewrite
Overview
Rewrite and fully own OpenChamber within the @prata.ma/pilot monorepo, separating it into a control plane (OpenChamber) and runtime plane (multiple OpenCode services). The rewrite replaces upstream OpenChamber dependencies with owned code, introduces proper multi-user auth (better-auth), a D1/Drizzle workspace registry, pinned-first runtime assignment, path-based workspace routing, and a TanStack Start frontend deployed to Cloudflare Workers.
This plan consolidates the "OpenChamber Rewrite" and "OpenChamber Services Integration" efforts into one unified execution plan.
Architecture Reference
- Primary architecture source:
@plan/ARCHITECTURE.md - This plan must align with architecture governance and ownership boundaries defined there.
- Re-baseline note (2026-02-15): this completed plan remains valid as implementation history, but canonical lineage authority is now external upstream (
/Users/bianpratama/Code/ai/openchamber) and governed by current architecture.
Goals
- Own all OpenChamber code and styling — no upstream dependency constraints for branding or behavior
- Integrate better-auth with GitHub + Google OAuth providers for real user identity
- Implement workspace registry backed by D1 with Drizzle ORM (swappable to Turso later)
- Support 4 workspace types:
local,git,mount(feature-flagged),sync - Implement pinned-first runtime assignment (workspace pinned to one runtime, manual rebalance only)
- Implement path-based workspace routing (
/<slug>→ workspace context, 404 for unknown slugs) - Evolve
@services/gatewayto D1-backed workspace + runtime API with assignment-aware proxy - Evolve
@services/computefrom single-workspace to multi-workspace model - Build TanStack Start frontend at
@services/appwith owned branding - Remove R2/FUSE mount as default requirement (keep as optional workspace type)
Non-Goals
- Automatic runtime rebalancing (manual only for initial rollout)
- Cross-runtime live migration automation
- Mandatory object-store mount for baseline startup
- Changes to
@services/sandboxsession orchestration (keep as-is initially) - OpenChamber UI package rewrite (may be phased out later, not in this plan)
- Auto-sync for
syncworkspace type (explicit/manual sync first)
Phases
- Phase 1: Foundation — Domain entities, Zod validation, slug rules, API contracts
- Phase 2: Database Layer — Drizzle schema in
@repo/domain, D1 driver, migrations - Phase 3: Gateway Evolution — D1 migration, workspace CRUD, runtime pool, assignment proxy
- Phase 4: Compute Evolution — Multi-workspace model, remove mount requirement
- Phase 5: Auth Integration — better-auth setup, middleware, ownership scoping
- Phase 6: TanStack Start Frontend —
@services/appscaffold, auth, workspace UI, branding - Phase 7: Demo & Deploy Simplification — Local-first defaults, simplified Docker/startup
Success
- Unknown workspace slug always returns 404 (no auto-create)
- Workspace CRUD API works for all 4 types (local, git, mount, sync)
- Workspace-scoped operations never fall back to
/root - better-auth identity enforced for workspace-scoped endpoints
- Frontend branding fully customizable from owned code
- Runtime assignment is deterministic and stable (pinned-first)
- Manual rebalance endpoint works (
POST /api/workspaces/:slug/reassign) - Compute hosts multiple workspaces under
/workspaces/<slug> - No R2/FUSE mount required for baseline startup
- D1 → Turso migration path is clear (Drizzle driver swap only)
Requirements
- Drizzle ORM for database schema + queries (in
@repo/domain) - D1 as initial database (Cloudflare Workers compatible)
- better-auth library for auth
- TanStack Start framework for frontend
- Cloudflare Workers deploy target for gateway + frontend
- Fly.io for compute runtimes
- Existing
@repo/domainentity patterns (Zod validation,@repo/basefields) - Existing
@services/gatewayHono router patterns - Existing
@services/computeDocker + Fly.io patterns
Boundaries
- Always: Use Drizzle schema as single source of truth for DB shape
- Always: Use
@repo/domainentity patterns (Zod,@repo/basefields) - Always: Use
@repo/sharedfor cross-service contracts - Always: Follow monorepo commit convention (
<type>(<scope>): <description>) - Ask first: Schema or API contract changes
- Ask first: Auth integration decisions
- Ask first: Branding/UI design choices
- Ask first: Adding new workspace types or runtime behaviors
- Never: Auto-create workspaces on unknown slug access
- Never: Use R2/FUSE mount as required default
- Never: Modify
@services/sandboxsession orchestration
Codebase Conventions
Directory Structure
- Source:
@source/(NOTsrc/) - Tests:
@test/(NOT__tests__/or co-located) - Mocks:
@mock/ - Build output:
build/(packages) ordist/(services)
Package Locations (npm name → filesystem)
@repo/domain→@packages/domain/@repo/base→@core/base/@repo/shared→@packages/shared/@pilot/gateway→@services/gateway/@pilot/compute→@services/compute/
Dependencies
pnpm-workspace.yamlusescatalogMode: strict- ALL dependencies must use
catalog:<name>syntax (e.g.,"hono": "catalog:backend") - Workspace packages use
"@repo/base": "workspace:*" - Drizzle is already in
catalog:db(drizzle-orm: 0.45.1,drizzle-kit: 0.31.9) - Check existing catalogs before adding new deps:
db,backend,build,infra,util, etc.
Domain Entity Pattern
Entities in @packages/domain/ use plain Zod schemas (NOT the Entity.define() system from @repo/base):
import { z } from '@repo/base/z'
export const FooStatusSchema = z.enum(['active', 'disabled'])
export type FooStatus = z.infer<typeof FooStatusSchema>
export const FooIdSchema = z.string().min(1)
export type FooId = z.infer<typeof FooIdSchema>
export const FooSchema = z.object({
id: FooIdSchema,
status: FooStatusSchema,
createdAt: z.string().datetime(),
})
export type Foo = z.infer<typeof FooSchema>
Conventions:
- Import Zod as
import { z } from '@repo/base/z'(NOT fromzoddirectly) - Each entity is one flat
.tsfile in@source/(not a subdirectory) - Naming:
<Name>Schemasuffix +type <Name> = z.infer<typeof <Name>Schema> - Sub-schemas for enums/IDs exported separately (e.g.,
WorkspaceStatusSchema,WorkspaceIdSchema) - Index.ts uses
export * from './<module>' - New entities need: source file + export in index.ts + export in package.json + entry in tsup build script
Package Export Convention
Each module gets its own export entry in package.json:
"./workspace": {
"types": "./build/workspace.d.ts",
"import": "./build/workspace.mjs",
"require": "./build/workspace.cjs"
}
Build script at scripts/tsup.ts uses glob @source/**/*.ts?(x) — new files are auto-included.
Gateway Conventions
- Framework:
Hono<{ Bindings: GatewayBindings }> - Bindings interface:
@source/types.ts - Import alias:
@/maps to@source/(e.g.,import { GatewayBindings } from '@/types') - Routes:
@source/routes/<name>.ts, exported asnew Hono<...>() - Middleware:
@source/middleware/<name>.ts - Current auth: simple
PILOT_API_TOKENbearer token comparison (NOT session-based) - Current workspace store:
@source/backends/workspace-docker/store.ts(KV-basedWorkspaceStoreclass) - Current bindings include:
SANDBOX: Fetcher,STATIC_ASSETS: R2Bucket,WORKSPACE_MAP: KVNamespace, plus string env vars
Compute Conventions
- Runtime: Bun (NOT Node.js) —
oven/bun:1-alpineDocker image - Entry:
start.sh+router.tsat package root (NOT insrc/or@source/) - Router uses
Bun.serve(), ports 4096 (OpenCode) + 8080 (router) - Single workspace at
WORKSPACE_DIR=/workspacewith oneWORKSPACE_ID
NETWORK.yml Port Assignment
- Next available port range:
14070-14079(afterdemo-openclaw-flyat 14064-14065) @services/appshould use port14070for dev
Questions
- Drizzle or raw SQL for DB layer? → Drizzle
- Auth parallel or blocking? → Parallel track (not blocking workspace API)
- Frontend location? →
@services/app(new service) - Drizzle schema location? →
@repo/domain - D1 database name and binding convention? → Database:
pilot-db, Binding:PILOT_DB - better-auth session storage? → D1 only (all tables via Drizzle adapter, single data source)
- Frontend-to-gateway communication? → Hybrid (auth via server functions, workspace CRUD direct to gateway)