3.8 KiB
opencode database guide
Database
- Schema: Drizzle schema lives in
src/**/*.sql.ts. - Naming: tables and columns use snake*case; join columns are
<entity>_id; indexes are<table>*<column>\_idx. - Migrations: generated by Drizzle Kit using
drizzle.config.ts(schema:./src/**/*.sql.ts, output:./migration). - Command:
bun run db generate --name <slug>. - Output: creates
migration/<timestamp>_<slug>/migration.sqlandsnapshot.json. - Tests: migration tests should read the per-folder layout (no
_journal.json).
opencode Effect guide
Instructions to follow when writing Effect.
Schemas
- Use
Schema.Classfor data types with multiple fields. - Use branded schemas (
Schema.brand) for single-value types.
Services
- Services use
ServiceMap.Service<ServiceName, ServiceName.Service>()("@console/<Name>"). - In
Layer.effect, always return service implementations withServiceName.of({ ... }), never a plain object.
Errors
- Use
Schema.TaggedErrorClassfor typed errors. - For defect-like causes, use
Schema.Defectinstead ofunknown. - In
Effect.gen, preferyield* new MyError(...)overyield* Effect.fail(new MyError(...))for direct early-failure branches.
Effects
- Use
Effect.gen(function* () { ... })for composition. - Use
Effect.fn("ServiceName.method")for named/traced effects andEffect.fnUntracedfor internal helpers. Effect.fn/Effect.fnUntracedaccept pipeable operators as extra arguments, so avoid unnecessaryflowor outer.pipe()wrappers.Effect.callback(notEffect.async) for callback-based APIs. The classicEffect.asyncwas renamed toEffect.callbackin effect-smol/v4.
Time
- Prefer
DateTime.nowAsDateovernew Date(yield* Clock.currentTimeMillis)when you need aDate.
Errors
- In
Effect.gen/fn, preferyield* new MyError(...)overyield* Effect.fail(new MyError(...))for direct early-failure branches.
Instance-scoped Effect services
Services that need per-directory lifecycle (created/destroyed per instance) go through the Instances LayerMap:
- Define a
ServiceMap.Servicewith astatic readonly layer(seeFileWatcherService,QuestionService,PermissionService,ProviderAuthService). - Add it to
InstanceServicesunion andLayer.mergeAll(...)insrc/effect/instances.ts. - Use
InstanceContextinside the layer to readdirectoryandprojectinstead ofInstance.*globals. - Call from legacy code via
runPromiseInstance(MyService.use((s) => s.method())).
Instance.bind — ALS context for native callbacks
Instance.bind(fn) captures the current Instance AsyncLocalStorage context and returns a wrapper that restores it synchronously when called.
Use it when passing callbacks to native C/C++ addons (@parcel/watcher, node-pty, native fs.watch, etc.) that need to call Bus.publish, Instance.state(), or anything that reads Instance.directory.
Don't need it for setTimeout, Promise.then, EventEmitter.on, or Effect fibers — Node.js ALS propagates through those automatically.
// Native addon callback — needs Instance.bind
const cb = Instance.bind((err, evts) => {
Bus.publish(MyEvent, { ... })
})
nativeAddon.subscribe(dir, cb)
Flag → Effect.Config migration
Flags in src/flag/flag.ts are being migrated from static truthy(...) reads to Config.boolean(...).pipe(Config.withDefault(false)) as their consumers get effectified.
- Effectful flags return
Config<boolean>and are read withyield*insideEffect.gen. - The default
ConfigProviderreads fromprocess.env, so env vars keep working. - Tests can override via
ConfigProvider.layer(ConfigProvider.fromUnknown({ ... })). - Keep all flags in
flag.tsas the single registry — just change the implementation fromtruthy()toConfig.boolean()when the consumer moves to Effect.