Pilot Plan

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/gateway to D1-backed workspace + runtime API with assignment-aware proxy
  • Evolve @services/compute from single-workspace to multi-workspace model
  • Build TanStack Start frontend at @services/app with 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/sandbox session orchestration (keep as-is initially)
  • OpenChamber UI package rewrite (may be phased out later, not in this plan)
  • Auto-sync for sync workspace 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/app scaffold, 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/domain entity patterns (Zod validation, @repo/base fields)
  • Existing @services/gateway Hono router patterns
  • Existing @services/compute Docker + Fly.io patterns

Boundaries

  • Always: Use Drizzle schema as single source of truth for DB shape
  • Always: Use @repo/domain entity patterns (Zod, @repo/base fields)
  • Always: Use @repo/shared for 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/sandbox session orchestration

Codebase Conventions

Directory Structure

  • Source: @source/ (NOT src/)
  • Tests: @test/ (NOT __tests__/ or co-located)
  • Mocks: @mock/
  • Build output: build/ (packages) or dist/ (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.yaml uses catalogMode: 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 from zod directly)
  • Each entity is one flat .ts file in @source/ (not a subdirectory)
  • Naming: <Name>Schema suffix + 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 as new Hono<...>()
  • Middleware: @source/middleware/<name>.ts
  • Current auth: simple PILOT_API_TOKEN bearer token comparison (NOT session-based)
  • Current workspace store: @source/backends/workspace-docker/store.ts (KV-based WorkspaceStore class)
  • 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-alpine Docker image
  • Entry: start.sh + router.ts at package root (NOT in src/ or @source/)
  • Router uses Bun.serve(), ports 4096 (OpenCode) + 8080 (router)
  • Single workspace at WORKSPACE_DIR=/workspace with one WORKSPACE_ID

NETWORK.yml Port Assignment

  • Next available port range: 14070-14079 (after demo-openclaw-fly at 14064-14065)
  • @services/app should use port 14070 for 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)