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 { join } from 'node:path'
import { defineDevCli } from 'devsess'
import { Effect } from 'effect'
const main = defineDevCli({
name: 'web',
dir: join(import.meta.dirname, '..'),
run: (ctx) =>
Effect.gen(function* () {
const port = yield* ctx.getStickyPort()
yield* ctx.runManagedSubprocess('bunx', ['vite'], {
env: { PORT: String(port) },
})
}),
})
main(process.argv)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.
- defineDevCli — the CLI scaffold behind
ctx, dev sessions, and what runs before yourrunfunction does. - devsess reference — every export, in full, once you're past the basics.