Runtime imports
Status: frozen for implementation on 2026-09-26 (operator decision). Remove this specification when the checks below run in hooks and CI and AGENTS.md, docs/architecture.md, and the effect-house overlay state the rule.
Rule (operator decision, 2026-09-26)
Only a Layer implementation touches the runtime directly: it imports node:*, bun:*, a bare Node builtin, or a provider SDK, or it reads the process, Bun, or Deno globals (operator decision, 2026-09-26).
A composition root (a main, a CLI, or test setup) chooses Layers. It imports platform Layer packages such as @effect/platform-bun, never a builtin.
Every other module imports neither. Effect's platform packages are the stock Layer implementations. The repository's own adapters are the others.
This makes the infrastructure-ports rule concrete: "No module outside a Layer imports a provider SDK, a platform binding, or a runtime-specific module."
Evidence (measured on 01db01e7)
- 723
node:imports outsideapps/server: about 35 in product source (packages/database,apps/backend), and the rest in tools, tests, and e2e runners. packages/databaseandapps/backendare declaredruntime-adapteras whole packages, sono-ambient-authoritystays silent there. The samenode:cryptoimport fails lint inpackages/domain.process.*is used 1,050 times.no-ambient-authorityin@phibkro/oxlint-effect-pluginalready rejects the wholeprocess,Bun, andDenoglobals, but only ineffect-librarygroups; the adapter, composition-root, and test groups skip it. tsgoprocess-envcovers onlyprocess.env, and only in core.- tsgo
node-builtin-importcovers onlyfs,path,child_process, andhttp, with or without the prefix. It does not covercrypto,os,net,timers/promises, orasync_hooks. - Effect
Crypto(rc.116) offers random bytes, digests, and UUIDs. It has no HMAC, AES-GCM, HKDF, or constant-time comparison. - Slice F added explicit
node:processandnode:bufferimports to 27 Bun roots to satisfy the platform declaration.
Where each use goes
| Use | Goes through |
|---|---|
| random bytes, UUIDs | Effect Crypto |
| hash of known bytes | @noble/hashes as a plain function (a total calculation, not a capability) |
| HMAC, AES-GCM, HKDF, constant-time comparison | one repository service with keys as its authority; its single live Layer uses WebCrypto |
| files, paths, processes, terminal, stdio, environment | Effect FileSystem, Path, ChildProcess, Terminal, Stdio, Config |
| time, sleep | Clock, Effect.sleep |
request-scoped state (AsyncLocalStorage) | Effect Context |
| IP parsing in the router | the HTTP adapter Layer, or a portable parser |
Buffer | Uint8Array and Effect Encoding |
test assertions (node:assert) | @effect/vitest |
process.env (408 uses) | Config through ConfigProvider.fromEnv |
process.stdout.write, stderr.write (215) | Stdio.stdout()/stderr(), Console, Terminal |
process.exit, exitCode (115) | BunRuntime.runMain. Its teardown maps the result to an exit code (0, 1, 130 on interrupt); Runtime.errorExitCode sets a custom code |
signal listeners: process.on, once, off, removeListener (75) | runMain turns SIGINT and SIGTERM into interruption, which runs Scope finalizers (acquireRelease, addFinalizer) |
process.argv (61) | Stdio.args, or effect/unstable/cli (Command, Flag, Argument) for a typed CLI |
process.kill of a child (55) | the handle from ChildProcessSpawner: kill, pid, exitCode, isRunning |
process.cwd (5) | Path.resolve(".") |
process.execPath, pid, getuid (60) | no Effect API: one repository RuntimeInfo service with a Bun Layer |
process.versions.bun (8) | not needed: the composition root chose the runtime |
An operation that no Effect API covers and no Layer can own becomes a registered exception (just exceptions) with a retirement trigger.
Phases
- Product source. Narrow the
runtime-adaptergroups inpackages/databaseandapps/backendto the modules that are Layers. Every other module there becomeseffect-library. Move the ~35 sites. - Composition roots. Measure whether the
@phibkro/oxlint-effect-plugincomposition-rootrole can reject builtins while admitting platform Layer packages. If it cannot, add that option in/srv/share/projects/oxlint-effect-plugin, release it, and pin it. Remove slice F'snode:processandnode:bufferimports and the bunextraAllowedModuleslist. - Tools, tests, and runners. First a measured inventory by module and file kind, committed into this spec. Then move each group. The e2e
.mjsrunners become typed.tswhen they move onto Effect services.
Done when
just lintfails on anode:or bare-builtin import, and on aprocess.*orBun.*read, in each of these: a non-Layer module inpackages/database,apps/backend, andpackages/domain; a composition root; a test; a tools script. It passes on the same import in a registered Layer module. Each case is recorded as a negative control.- A composition root that provides
BunServices.layerpasses. - The only remaining
node:andbun:imports are in Layer modules, or are registered exceptions. just checkand the hosted Checks and Tests workflows pass.
Order
Run after certificates, because phase 1 edits the same backend and database files. The construct-contracts slice has landed, and the Fumadocs slice may land first. Public surface comes after this.