# fix\(server\): refuse to start a second server against a live data directory · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/commit/53dd74b2067df1733d1af3dcb3f4c050423f39e7)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

Requested revision: 53dd74b2067df1733d1af3dcb3f4c050423f39e7

Requested commit: 53dd74b2067df1733d1af3dcb3f4c050423f39e7

Commit: 53dd74b2067df1733d1af3dcb3f4c050423f39e7

Tree: 0b6873c25daae936b52798054f43a58a2eb9738d

Author: NoahLinckeScout

Committer: NoahLinckeScout

## Message

```
fix(server): refuse to start a second server against a live data directory

Two servers pointed at one `--base-dir` both open `state.sqlite` and both write
`settings.json`, and they overwrite each other. Observed: a desktop app
auto-updated to a newer server while the old one was still running, the new
process found its port taken, silently bound a random one, and ran blind against
shared state. The visible symptom was a settings toggle that would not stick --
hours away from the cause, and nothing about it is detectable afterwards.

So refuse at startup. The lock is claimed before anything binds a port or opens
the database, and is provided into `HttpServerLive` rather than merged beside it
so the ordering is structural: the lock is a dependency of the thing it protects.

An advisory `flock` would be the better primitive, since the kernel drops it when
the holder dies. Node has no binding for it and a native dependency for one lock
is the worse trade, so this is an atomically created file holding the owner's
identity, with liveness checked by signal 0. The tradeoff is stated in the module:
a killed server whose pid is later reused blocks startup until the file is
removed, which is the safe direction, and the message names the file.

A lock whose owner is gone, or which a crash tore in half mid-write, is reclaimed
rather than treated as permanent. Reclaiming re-races the exclusive create, so two
servers starting together still produce one winner. Release only removes a lock
this process still owns, so a successor is never evicted.

The bound port is stamped onto the lock afterwards purely so a later server's
refusal names an address the user can open rather than just a pid.

Verified end to end against two real servers: the second refuses with the message
below, exits 1, and never binds; shutdown releases; restart is unblocked.

  Another T3 Code server is already using this data directory.

    data directory: /tmp/t3-smoke-basedir/userdata
    held by:        pid 285163, listening on port 39977
    since:          2026-08-27T16:59:03.329Z

```

## Parents

- [549d182aaadbf0e1e195d1a8cdc1805200e6751e](https://git.cafe/pingdotgg/t3code/commit/549d182aaadbf0e1e195d1a8cdc1805200e6751e?format=markdown)

[Source at this commit](https://git.cafe/pingdotgg/t3code/tree/53dd74b2067df1733d1af3dcb3f4c050423f39e7?format=markdown)
