Practical reference for extracting a future `packages/server` from the current `packages/opencode` monolith while `packages/core` is still being migrated to Effect.
This document is intentionally execution-oriented.
It should give an agent enough context to land one incremental PR at a time without needing to rediscover the package strategy, route migration rules, or current constraints.
## Goal
Create `packages/server` as the home for:
- HTTP contract definitions
- HTTP handler implementations
- OpenAPI generation
- eventual embeddable server APIs for Node apps
Do this without blocking on the full `packages/core` extraction.
## Future state
Target package layout:
-`packages/core` - all opencode services, Effect-first source of truth
-`packages/server` - opencode server, with separate contract and implementation, still producing `openapi.json`
-`packages/cli` - TUI + CLI entrypoints
-`packages/sdk` - generated from the server OpenAPI spec, may add higher-level wrappers
-`packages/plugin` - generated or semi-hand-rolled non-Effect package built from core plugin definitions
Desired user stories:
- import from `core` and build a custom agent or app-specific runtime
- import from `server` and embed the full opencode server into an existing Node app
- spawn the CLI and talk to the server through that boundary
## Current state
Everything still lives in `packages/opencode`.
Important current facts:
- there is no `packages/core` or `packages/cli` workspace yet
This means the package split should start from an extraction path, not from greenfield package ownership.
## Structural reference
Use `anomalyco/opentunnel` as the structural reference for `packages/server`.
The important pattern there is:
-`packages/core` owns services and domain schemas
-`packages/server/src/definition/*` owns pure `HttpApi` contracts
-`packages/server/src/api/*` owns `HttpApiBuilder.group(...)` implementations and server-side middleware wiring
-`packages/server/src/index.ts` becomes the composition root only after the server package really owns runtime hosting
Relevant `opentunnel` files:
-`packages/server/src/definition/index.ts`
-`packages/server/src/definition/tunnel.ts`
-`packages/server/src/api/index.ts`
-`packages/server/src/api/tunnel.ts`
-`packages/server/src/api/client.ts`
-`packages/server/src/index.ts`
The intended direction here is the same, but the current `opencode` package split is earlier in the migration.
That means:
- we should follow the same `definition` and `api` naming
- we should keep contract and implementation as separate modules from the start
- we should postpone the runtime composition root until `packages/core` exists enough to support it cleanly
## Key decision
Start `packages/server` as a contract and implementation package only.
Do not make it the runtime host yet.
Why:
-`packages/core` does not exist yet
- the current server host still lives in `packages/opencode`
- moving host ownership immediately would force a large package and runtime shuffle while Effect service extraction is still in flight
- if `packages/server` imports services from `packages/opencode` while `packages/opencode` imports `packages/server` to host routes, we create a package cycle immediately
Short version:
1. create `packages/server`
2. move pure `HttpApi` contracts there
3. move handler factories there
4. keep `packages/opencode` as the temporary Hono host
5. merge `packages/server` OpenAPI with the legacy Hono OpenAPI during the transition
6. move server hosting later, after `packages/core` exists enough
## Dependency rule
Phase 1 rule:
-`packages/server` must not import from `packages/opencode`
Allowed in phase 1:
-`packages/opencode` imports `packages/server`
-`packages/server` accepts host-provided services, layers, or callbacks as inputs
-`packages/server` may temporarily own transport-local placeholder schemas when a canonical shared schema does not exist yet
Future rule after `packages/core` exists:
-`packages/server` imports from `packages/core`
-`packages/cli` imports from `packages/server` and `packages/core`
-`packages/opencode` shrinks or disappears as package responsibilities are fully split
## HttpApi model
Use Effect v4 `HttpApi` as the source of truth for migrated HTTP routes.
Important properties from the current `effect` / `effect-smol` model:
-`HttpApi`, `HttpApiGroup`, and `HttpApiEndpoint` are pure contract definitions
- handlers are implemented separately with `HttpApiBuilder.group(...)`
- OpenAPI can be generated from the contract alone
- auth and middleware can later be modeled with `HttpApiMiddleware.Service`
- SSE and websocket routes are not good first-wave `HttpApi` targets
This package split should preserve that separation explicitly.
Default shape for migrated routes:
- contract lives in `packages/server/src/definition/*`
- implementation lives in `packages/server/src/api/*`
- host mounting stays outside for now
## OpenAPI rule
During the transition there is still one spec artifact.
Default rule:
-`packages/server` generates OpenAPI from `HttpApi` contract
-`packages/opencode` keeps generating legacy OpenAPI from Hono routes
- the temporary exported server spec is a merged document
-`packages/sdk` continues consuming one `openapi.json`
Merge safety rules:
- fail on duplicate `path + method`
- fail on duplicate `operationId`
- prefer explicit summary, description, and operation ids on all new `HttpApi` endpoints
Practical implication:
- do not make the SDK consume two specs
- do not switch SDK generation to `packages/server` only until enough of the route surface has moved
## Package shape
Minimum viable `packages/server`:
-`src/index.ts`
-`src/definition/index.ts`
-`src/definition/api.ts`
-`src/definition/question.ts`
-`src/api/index.ts`
-`src/api/question.ts`
-`src/openapi.ts`
-`src/bridge/hono.ts`
-`src/types.ts`
Later additions, once there is enough real contract surface:
-`src/api/client.ts`
- runtime composition in `src/index.ts`
Suggested initial exports:
-`api`
-`openapi`
-`questionApi`
-`makeQuestionHandler`
Phase 1 responsibilities:
- own pure API contracts
- own handler factories for migrated slices
- own contract-generated OpenAPI
- expose host adapters needed by `packages/opencode`
Phase 1 non-goals:
- do not own `listen()`
- do not own adapter selection
- do not own global server middleware
- do not own websocket or SSE transport
- do not own process bootstrapping for CLI entrypoints
- all Effect services until they move to `packages/core`
## Placeholder schema rule
`packages/core` is allowed to lag behind.
Until shared canonical schemas move to `packages/core`:
- prefer importing existing Effect Schema DTOs from current locations when practical
- if a route only needs a transport-local type and moving the canonical schema would create unrelated churn, allow a temporary server-local placeholder schema
- if a placeholder is introduced, leave a short note so it does not become permanent
The default rule from `schema.md` still applies:
- Effect Schema owns the type
-`.zod` is compatibility only
- avoid parallel hand-written Zod and Effect definitions for the same migrated route shape
## Host boundary rule
Until host ownership moves:
- auth stays at the outer Hono app level
- compression stays at the outer Hono app level
- CORS stays at the outer Hono app level
- instance and workspace lookup stay at the current middleware layer
-`packages/server` handlers should assume the host already provided the right request context
- do not redesign host middleware just to land the package split
This matches the current guidance in `http-api.md`:
- keep auth outside the first parallel `HttpApi` slices
- keep instance lookup outside the first parallel `HttpApi` slices
- keep the first migrations transport-focused and semantics-preserving
## Route selection rules
Good early migration targets:
-`question`
-`provider` auth read endpoint
-`config` providers read endpoint
- small read-only instance routes
Bad early migration targets:
-`session`
-`event`
-`pty`
- most `global` streaming or process-heavy routes
- anything requiring websocket upgrade handling
- anything that mixes many mutations and streaming in one file