devsess
devsess is a library for Node dev scripts, built on Effect, that gives every parallel checkout of your project — every git worktree, every AI agent working in its own copy — an isolated dev session.
What it solves
You're not running one dev environment — you're running several at once: a couple of git worktrees, and increasingly, a handful of AI agents each working in their own worktree. Point them at the same project and they collide:
- Two agents both want port 3000. Only one of them gets it.
- Two agents point at the same local Postgres. One runs a migration and destroys what the other seeded.
devsess resolves this by giving each checkout its own session directory, holding that instance's port, database, and state:
- sess.json
- pglite
- sess.json
- pglite
That isolation falls out of where the data lives, not from any session-switching feature: each checkout is a different dir, so each gets its own <dir>/.data/sessions/. Within a single checkout, devsess still resumes whichever session was used most recently — there's no flag to pick a specific one.
devsess only targets that layer: local dev scripts on Node. It isn't a process supervisor, a task runner, or a replacement for Docker Compose.
What you get
- Ports that are free, come back the same on every restart, and can be discovered by other services. Ports that survive restarts →
- A per-session Postgres, with no database server to run yourself. A database per session →
- State shared between services. Turborepo orders your tasks; it doesn't share what they learned. devsess does. Wiring services together →
- Ordinary TypeScript around startup. Do work before or after your server comes up — rewrite
.mcp.jsononce you know the port, say. Running your dev server →
What it looks like
import { CurrentSession, DevSessions, getStickyPort, runManagedSubprocess } from 'devsess'
import { NodeRuntime, NodeServices } from '@effect/platform-node'
import { Effect } from 'effect'
import { Command } from 'effect/unstable/cli'
const web = Command.make('web', {}, () =>
Effect.gen(function* () {
const session = yield* CurrentSession
const port = yield* getStickyPort(session)
yield* runManagedSubprocess('bunx', ['vite'], {
env: { PORT: String(port) },
})
}).pipe(Effect.provide(CurrentSession.layer)),
)
Command.run(web, { version: '0.1.0' }).pipe(
Effect.provide(DevSessions.layer),
Effect.provide(NodeServices.layer),
Effect.scoped,
NodeRuntime.runMain,
)The port comes back the same as last time, if it's still free. vite doesn't outlive the script — Ctrl-C kills it too.
Where to go next
- Getting Started — install devsess and get the script above running for real.
- Writing a dev CLI —
DevSessions,CurrentSession, dev sessions, and what a scope buys you. - devsess reference — every export, in full, once you're past the basics.