Effect 4 reference projects (studied 2026-09-18)
These projects show Effect service, test, CLI, and lifecycle patterns. The study dates are 2026-09-18 and 2026-09-20.
Versions below come from each linked repo’s lockfile or catalog on those dates. Current compatibility needs a fresh check.
See current pins, the current reference machine packages/core/src/inspect-machine.ts, and lifecycles are machines20.
For the browser client, Alchemy resources, database, auth, and Worker tests, see Effect + Alchemy peer patterns.
Projects
| Repo | Stars | Effect | Runtime | Why it matters to us |
|---|---|---|---|---|
| pingdotgg/ | 23k | 4.0.0-rc.115 (catalog) | Node 24, pnpm 11, TS 7.0.2 | Same Effect, TS, and pnpm pins as rat-stack. Closest sibling. Heaviest @effect/vitest user (479 files). |
| anomalyco/ | 208k | 4.0.0-beta.83 (patched) | Bun | Architecture reference only. Older beta: still has ServiceMap.Service remnants and Schema.TaggedErrorClass. Do not copy API names from it. |
| tim-smart/ | 130 | 4.0.0-beta.94 | Node, pnpm, tsgo | Effect core maintainer’s CLI. Uses effect/unstable/cli, Context.Service + static layer, @effect/language-service. Closest shape to our CLI. |
| kitlangton/ | 1.1k | 4.0.0-beta.90 | Bun | Kit’s TUI. Context.Service, Schema.TaggedError, @effect/language-service, oxlint + oxfmt. |
| alchemy-run/ | 1.3k | peer >=4.0.0-rc.115 | Node | Our infra layer. Peer-depends on @effect/language-service ^0.87. |
| cevr/ | 72 | peer >=4.0.0-rc.112 <5 | any | Schema-first state machines native to Effect. The Effect-world answer to XState. |
Other Effect 4 adopters found via GitHub code search for effect/unstable imports: alchemy-run/distilled, MapleTechLabs/maple, tim-smart/receipts, tim-smart/dfx, kitlangton/motel, kitlangton/stack, leonitousconforti/the-moby-effect, crosshatch/crosshatch, anomalyco/effect-http-recorder, Effectful-Tech/clanka.
Shared patterns
- Service tags are
Context.Serviceclasses with a namespaced string id ("t3/persistence/Services/ProjectionState","lalph/Editor","@opencode/ServerAuthConfig").Effect.Serviceis effectively gone (t3code: 4 files, opencode: 0).ServiceMapwas a beta-only name; rc.115 exportsContext.Service,Context.Reference,Context.Key. static layer = Layer.effect(this, this.make).pipe(Layer.provide(...))co-located on the service class (lalph, ghui, Kit’s effect-solutions). t3code splitsServices/(tag + shape interface) fromLayers/(implementation) per module.- Errors are
Schema.TaggedErrorwithoverride get message()andcause: Schema.Defect(). t3code keeps wire-crossing errors inpackages/contractsso they serialize. Same shape rat-stack already uses inpackages/core/src/stats.ts. Effect.fn("name")for traced functions,Effect.fnUntracedfor hot paths. Straight fromnode_modules/effect/AGENTS.md.- Tests use
@effect/vitest:it.effect,it.layer(TestLayer),Layer.mock(Tag)({...}). t3code enforces this with a custom oxlint ruleno-manual-effect-runtime-in-teststhat bansEffect.runPromiseandManagedRuntime.makein test files. @effect/language-serviceis on in every Node project (lalph, ghui, t3code, alchemy peer). t3code turns ~25 diagnostics toerror:leakingRequirements,missingEffectServiceDependency,importFromBarrel,preferSchemaOverJson,schemaSyncInEffect,nodeBuiltinImport,globalDate/Console/Fetch/Timers/Random(InEffect),unknownInEffectCatch. With TS 7 this runs through@effect/tsgoandeffect-tsgo patchinprepare, so diagnostics appear intsc --noEmit, not only in the editor.- CLI entry is
effect/unstable/cliCommand.runWith+NodeRuntime.runMain. lalph and t3code scripts do exactly whatapps/cli/src/cli.tsdoes. opencode still uses yargs. - Vendored reference source is pinned to the installed version. t3code’s
scripts/sync-reference-repos.tsderives the git tag from the pnpm catalog entry and mirrors into.repos/. Its AGENTS.md says to read.repos/effect-smol/LLMS.mdbefore writing Effect. - Config via Effect
Config. opencode’sConfigService.Service(id, { field: Config.string("ENV") })generates a service with.layer(reads env) and.configLayer(values)(tests). Small and worth stealing.
XState 6 and @xstate/effect (verified 2026-09-18)
None of the reference apps use XState. t3code models lifecycle as decider, projector, and reactor. opencode uses a LayerNode graph. cevr/effect-machine is the community Effect-native machine. But XState itself now ships an Effect 4 bridge for v6, and it is the one that matters for this stack.
- Package:
@xstate/effectatpackages/xstate-effecton thenextbranch ofstatelyai/xstate. PR #5687 merged 2026-09-15; #5691 added Effect Schema support. Author David Khourshid7. - Pins: peer
effect ^4.0.0-rc.115andxstateworkspace (6.0.0-alpha.58). Both match rat-stack exactly. - On npm since 2026-09-19 17:00 UTC. Changesets versioned it to
0.1.0-alpha.2on 2026-09-17; that day’s Publish run failed on@xstate/effectwithE404 PUT https://registry.npmjs.org/@xstate%2feffect, a first-publish scope20 problem. The registry tarball published two days later is byte-identical to the one built from the tag (same 20 files, samepackage.json, samedist). rat-stack swappedpackages/coreto the registry version on 2026-09-20 (commit noted invendor/README.md); the version-scopedminimumReleaseAgeExcludeentry stays, like every other prerelease pin. Issue #5725 has no maintainer reply yet. - Shape:
createEffectActor(machine)returnsEffect<EffectActor, never, R | Scope>and drives XState 6’s puretransitionthroughxstate/durableon an Effect fiber. Mailbox is aQueue, timers areEffect.sleepon the EffectClocksoTestClockdrivesafter, declared Effect actions fork in the actor scope, andRis collected from declared actions and actors viaRequirementsFrom.fromEffect,fromEffectStream,fromEffectEventStreamturn Effects and Streams into actor logic with typedEandR.setupEffectaccepts Effect schemas and Effect-returning actions.send,waitFor,join,snapshots,inspect,deadLettersare the Effect-native observation API.taggedStategivesMatch.exhaustiveover states. An actor as a service isLayer.effect(Tag, createEffectActor(machine)). - Fence39: the repo ships
scripts/oxlint-plugin-xstate-effect.mjs(172 lines) with rulexstate-effect/no-inline-effect, catching an Effect returned from an inlineenqcallback (created and discarded, since XState awaits only promises) and inline Effect logic passed toenq.spawn. Lift it. - Gotchas from the docs: Effect-backed logic must start under
createEffectActor, nevercreateActor. Only declared actions and actors contribute toR; inline Effects inferneverand fail at runtime. Restoring a persisted snapshot into a new interpreter is not supported yet; the durable loop is the stated path. Requirements collection stops at 10 levels of machine nesting.
Upstream contribution filed (2026-09-18)
Issue https://github.com/statelyai/xstate/issues/5725 (Joel’s account, labelled agent-generated, no PR by decision). Patch gist https://gist.github.com/joelhooks/99d193172a03609e67b4ef5849e8bbb1. Watch the issue for maintainer response before touching the lint plugin again.
The public patch gist records the proposed changes against XState next at 0748e1b. It ports the oxlint plugin to TypeScript on @oxlint/plugins@^1.76.0 (same version as their pinned oxlint), repoints .oxlintrc.json, the lint fixture config, lint.test.ts, and two docs mentions, and corrects the README and docs/testing-and-errors.md claim that join fails with “the machine’s typed error”: a machine’s ErrorFrom is unknown, and docs/observing-actors.md now says so and recommends final states plus orDie or catchAll. Verified in their tree: manypkg check, frozen install with postinstall, lint.test.ts 2/2, pnpm lint, oxfmt --check. Lock diff is 9 lines, spliced by hand because a non-frozen pnpm install here strips every tarball: field their lock keeps. The public issue and patch above are the contribution record.
Barrel and subpath imports
- Projects differ on barrel and subpath imports. t3code uses
import * as Effect from "effect/Effect"withimportFromBarrel: errorfor client bundle size. lalph, ghui, opencode useimport { Effect } from "effect". For a Node CLI the barrel is fine.
Applied 2026-09-18 (second slice, all remaining gaps)
- Language service in the fence.
@effect/tsgo@0.45.0(supports TS 7.0.2) patches the tsgo binary inprepare;tsconfig.base.jsoncarries the diagnostic set aterror. Verified: a probe withconsole.log,Date.now(), andfetchinsideEffect.genfailstscwithglobalConsoleInEffect,globalDateInEffect,globalFetchInEffect; the earlier core test file failed onnodeBuiltinImportandasyncFunctionuntil converted. Two things a probe did not catch, correctly:Effect.tryPromisewithout a catch already types its error asUnknownError, and returning a service from an exported Effect is not whatleakingRequirementschecks (it checks service method signatures). Escape hatch is// @effect-diagnostics-next-line <rule>:off, used exactly twice:classSelfMismatchon the generic config-service factory, andanyUnknownInErrorContextat the twojoinsites (machineErrorFromisunknown, issue #5725). The e2e test keepsnode:imports with a file-levelnodeBuiltinImport:offbecause it spawns the built binary as a black box. - Reference service.
packages/core/src/file-inspector.ts:Context.ServicewithmakecapturingFileSystem, methods withR = never,static layer. The machine’s actor usesFileInspector.use(...), soRequirementsFromisFileInspectorandcli.tsis the one composition root16:Layer.provideMerge(FileInspector.layer, NodeServices.layer). - Config service. opencode’s pattern on rc.115 in
packages/core/src/config-service.ts(Config.allneeds one cast for a generic record;Layer.succeed(this, input)replacesthis.of).AppConfigmirrors.env.schema. Tests useConfigProvider.layer(ConfigProvider.fromUnknown({...})). @effect/vitest. All Effect tests areit.effectorit.layer(TestLayer)(name, (test) => ...). Temp files come fromFileSystem.makeTempDirectoryScoped, nonode:fs. Ruleeffect-tests/no-manual-effect-runtime-in-tests(t3code port, no debt ceiling) bansEffect.run*andManagedRuntime.makein test files.itis renamedtestinsideit.layercallbacks becauseno-shadowis on.- Mirrors.
vendor-agent-sources.shderives refs from the package pins and links.agent-sources/effect(pi-effect’s path) to the pinned mirror; this resolved the stale beta.99 mirror. AGENTS.md now points atnode_modules/effect/AGENTS.mdfirst. - Gotchas hit.
classSelfMismatchfires on any generic service factory.no-barrel-filecountsexport * asre-exports against Effect’s whole module graph, so namespaces are exported as a const object. pnpm 11’sverify-deps-before-runre-serializes a lockfile it did not write (dropstarball:fields), which matters when preparing patches for repos whose lock keeps them.
Sweep 2026-09-20
Joel asked for a library sweep. Bumped to the newest tag on each pinned line: effect, @effect/platform-node, @effect/vitest rc.115 to rc.116; alchemy beta.78 to beta.79; turbo 2.10.13 to 2.11.2; varlock 1.19.0 to 1.20.0; oxlint-tsgolint 7.0.2001 to 7.0.2002; @earendil-works/pi-coding-agent 0.85.1 to 0.86.1; @types/node 24.13.5 to 24.13.6 (staying on the Node 24 line; 26.x exists). Already latest: TypeScript 7.0.2, vitest 5.0.1, oxlint and @oxlint/plugins 1.83.0, ultracite 7.12.0, oxfmt 0.68.0, lefthook 2.1.14, xstate 6.0.0-alpha.58, @xstate/effect 0.1.0-alpha.2, @effect/tsgo 0.45.0. Gate green with no source change for rc.116. pnpm wrote the six @earendil-works/*@0.86.1 entries into minimumReleaseAgeExclude itself (“loose mode”); the dead beta.78 entries were removed by hand. capnp-es still wants TypeScript 5 or 6 under alchemy; pre-existing, ignored.
Two fence findings on the way:
turbo run checknever linted. Each package’scheckscript runs onlytypecheck, sopnpm turbo run check test build, the documented gate and what CI runs, skippedultracite checkandoxfmt --check. Only the rootpnpm check, which lefthook runs on commit, linted. Every lint failure this week was caught by the hook, never by CI. Fixed by a rootlintscript and a//#lintturbo task thatcheckdepends on; the gate now runs 15 tasks.oxlint-tsgolint7.0.2002 reports a multi-linex as unknown as Ton the line holding the secondas, out of reach of adisable-next-lineabove the statement.to-toolkit.tsnow binds the value asunknownon its own line and casts on the next. Pattern for the other cast sites if the rule moves again.
The vendor script’s xstate pin still pointed at apps/cli/package.json after the machine moved to packages/core; it resolved xstate@undefined. Fixed, mirrors refreshed to rc.116 and beta.79.
Gaps in rat-stack against this bar (as found 2026-09-18, all now closed)
- No
@effect/language-service. This is the single largest missing fence for “agents write good Effect”. Every reference project has it. - No service or layer in the example.
readFileStatsyieldsFileSystemdirectly. Agents most often getContext.Service+Layer+Layer.mockwrong in v4, and the template shows none of it. - Plain vitest, not
@effect/vitest.@effect/vitest@4.0.0-rc.115is already in the pnpm store via alchemy but not declared. Noit.effectorit.layerexample exists. - Closed 2026-09-18.
@xstate/effect@0.1.0-alpha.2is vendored asvendor/xstate-effect-0.1.0-alpha.2.tgz(built from tagxstate@6.0.0-alpha.58, seevendor/README.mdfor the npm swap rule).apps/cli/src/inspect-machine.tsis the reference machine:setupEffectwith a declaredfromEffectactor,readingtoinspectedorunreadable,joinplusEffect.orDielifting the outcome back into the Effect error channel. Its oxlint rule is ported toscripts/oxlint-plugin-xstate-effect.tson@oxlint/plugins@1.83.0and wired asxstate-effect/no-inline-effect: error. Two things learned on the way: the machine context must be declared viatypes<Ctx>()orschemas.context, otherwisecreateMachine’s function-formcontextfalls back toMachineContextand the output type collapses toany; andErrorFromof a machine isunknown, sojoinneedsorDie(or an explicit catch) to keep a typed error channel. Also fixed on the way:Flag.Booleanineffect/unstable/clirc.115 is required unlessFlag.withDefault(false)is applied; the template’s--jsonflag was silently mandatory. - Two Effect mirrors, one stale.
.agent_sources/github.com/Effect-TS/effectis at rc.115 (correct)..agent-sources/effect(thepi-effecttool’s mirror) is at beta.99..agent_sources/README.mdstill says rc.110. Effect’s own AGENTS.md warns against unrelated copies. node_modules/effect/AGENTS.mdis not referenced. It ships in the package (409 lines), is always version-matched, and is what other Effect 4 migrations point agents at first.- Vendor script pins by hand. t3code derives the ref from the catalog. Ours can derive it from
packages/core/package.json.
Current compatibility check (2026-10-09)
- Runtime packages stay on Effect 4.0.0. Foldkit16’s exact Effect peers and platform dependencies must share one runtime.
- Alchemy and its Better Auth16 adapter use beta.81. XState uses alpha.65 and the Effect bridge uses alpha.7.
- Bridge alpha.7 supports persisted snapshots through
createEffectActor. Restored machines keep their state and context without repeating entry actions. - Pending delayed transitions keep their original deadlines on Effect’s Clock. Running Effect tasks and streams restart; handlers must tolerate that restart.
- This upgrade changes no application machine to snapshot-based hydration. The September restore limitation above records the earlier source version.
- The compiler patcher moves to tsgo 0.51.1 and still patches TypeScript 7.0.2. The obsolete
importFromBarrel:offsetting is removed. - The base diagnostics remain shared. Fifteen child configs allow only unstable modules actually warned about in their scope.
- Warning-only compiler output still fails. Probes against the core config confirm that inherited errors and an unlisted unstable API remain failures.
The peer roster records current default-branch evidence. The September study and contribution links above keep their original versions.
Sources
- t3code:
tsconfig.base.json,oxlint-plugin-t3code/rules/*.ts,apps/server/src/persistence/{Services,Layers}/ProjectionState.ts,scripts/sync-reference-repos.ts,AGENTS.md - opencode:
packages/opencode/src/effect/config-service.ts,packages/core/src/effect/{layer-node,runtime}.ts,AGENTS.md - lalph:
src/Editor.ts,package.json,tsconfig.json - Effect rc.115:
node_modules/.pnpm/effect@4.0.0-rc.115/node_modules/effect/{AGENTS.md,src/Context.ts,src/Schema.ts}
Last changed 2026-10-09 in 3bab7b3. Source on GitHub. Change log.