- 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).
Use these rules when writing or migrating Effect code.
See specs/effect-migration.md for the compact pattern reference and examples.
- Use
Effect.gen(function* () { ... })for composition. - Use
Effect.fn("Domain.method")for named/traced effects andEffect.fnUntracedfor internal helpers. Effect.fn/Effect.fnUntracedaccept pipeable operators as extra arguments, so avoid unnecessary outer.pipe()wrappers.- Use
Effect.callbackfor callback-based APIs. - Prefer
DateTime.nowAsDateovernew Date(yield* Clock.currentTimeMillis)when you need aDate.
- Use
Schema.Classfor multi-field data. - Use branded schemas (
Schema.brand) for single-value types. - Use
Schema.TaggedErrorClassfor typed errors. - Use
Schema.Defectinstead ofunknownfor defect-like causes. - In
Effect.gen/Effect.fn, preferyield* new MyError(...)overyield* Effect.fail(new MyError(...))for direct early-failure branches.
- Use
makeRuntime(fromsrc/effect/run-service.ts) for all services. It returns{ runPromise, runFork, runCallback }backed by a sharedmemoMapthat deduplicates layers. - Use
InstanceState(fromsrc/effect/instance-state.ts) for per-directory or per-project state that needs per-instance cleanup. It usesScopedCachekeyed by directory — each open project gets its own state, automatically cleaned up on disposal. - If two open directories should not share one copy of the service, it needs
InstanceState. - Do the work directly in the
InstanceState.makeclosure —ScopedCachehandles run-once semantics. Don't add fibers,ensure()callbacks, orstartedflags on top. - Use
Effect.addFinalizerorEffect.acquireReleaseinside theInstanceState.makeclosure for cleanup (subscriptions, process teardown, etc.). - Use
Effect.forkScopedinside the closure for background stream consumers — the fiber is interrupted when the instance is disposed.
- In effectified services, prefer yielding existing Effect services over dropping down to ad hoc platform APIs.
- Prefer
FileSystem.FileSysteminstead of rawfs/promisesfor effectful file I/O. - Prefer
ChildProcessSpawner.ChildProcessSpawnerwithChildProcess.make(...)instead of custom process wrappers. - Prefer
HttpClient.HttpClientinstead of rawfetch. - Prefer
Path.Path,Config,Clock, andDateTimewhen those concerns are already inside Effect code. - For background loops or scheduled tasks, use
Effect.repeatorEffect.schedulewithEffect.forkScopedin the layer definition.
Use Effect.cached when multiple concurrent callers should share a single in-flight computation rather than storing Fiber | undefined or Promise | undefined manually. See specs/effect-migration.md for the full pattern.
Instance.bind(fn) captures the current Instance AsyncLocalStorage context and restores it synchronously when called.
Use it for native addon callbacks (@parcel/watcher, node-pty, native fs.watch, etc.) that need to call Bus.publish or anything that reads Instance.directory.
You do not need it for setTimeout, Promise.then, EventEmitter.on, or Effect fibers.
const cb = Instance.bind((err, evts) => {
Bus.publish(MyEvent, { ... })
})
nativeAddon.subscribe(dir, cb)