pgserve 1.2.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.genie/brainstorms/pgserve-v2/DESIGN.md +174 -0
  2. package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +99 -0
  3. package/.genie/wishes/pgserve-v2/WISH.md +442 -0
  4. package/.genie/wishes/release-system-genie-pattern/WISH.md +9 -9
  5. package/.genie/wishes/release-system-genie-pattern/validation.md +43 -10
  6. package/.github/workflows/ci.yml +10 -6
  7. package/.github/workflows/release.yml +1 -1
  8. package/.github/workflows/version.yml +4 -4
  9. package/CHANGELOG.md +150 -0
  10. package/Makefile +12 -12
  11. package/README.md +216 -10
  12. package/bin/pgserve-wrapper.cjs +3 -3
  13. package/bin/{pglite-server.js → postgres-server.js} +258 -1
  14. package/bun.lock +0 -3
  15. package/ecosystem.config.cjs +3 -3
  16. package/eslint.config.js +2 -0
  17. package/knip.json +1 -1
  18. package/package.json +4 -5
  19. package/scripts/test-bun-self-heal.sh +10 -10
  20. package/src/admin-client.js +171 -0
  21. package/src/audit.js +168 -0
  22. package/src/control-db.js +313 -0
  23. package/src/daemon-control.js +408 -0
  24. package/src/daemon-shared.js +18 -0
  25. package/src/daemon-tcp.js +296 -0
  26. package/src/daemon.js +629 -0
  27. package/src/fingerprint.js +453 -0
  28. package/src/gc.js +351 -0
  29. package/src/index.js +31 -0
  30. package/src/protocol.js +131 -0
  31. package/src/router.js +8 -0
  32. package/src/sdk.js +137 -0
  33. package/src/tenancy.js +75 -0
  34. package/src/tokens.js +102 -0
  35. package/tests/audit.test.js +189 -0
  36. package/tests/benchmarks/runner.js +430 -754
  37. package/tests/control-db.test.js +285 -0
  38. package/tests/daemon-fingerprint-integration.test.js +111 -0
  39. package/tests/daemon-pr24-regression.test.js +198 -0
  40. package/tests/fingerprint.test.js +249 -0
  41. package/tests/fixtures/240-orphan-seed.sql +30 -0
  42. package/tests/orphan-cleanup.test.js +390 -0
  43. package/tests/sdk.test.js +71 -0
  44. package/tests/tcp-listen.test.js +368 -0
  45. package/tests/tenancy.test.js +403 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,150 @@
1
+ # Changelog
2
+
3
+ All notable changes to `pgserve` are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
5
+ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 2.0.0 — Unreleased
8
+
9
+ > The release date will replace "Unreleased" when the v2.0.0 release workflow
10
+ > fires. The CHANGELOG is committed ahead of the release trigger so consumers
11
+ > can review the migration plan before the artifact lands on npm.
12
+
13
+ ### Pin guidance (read this first)
14
+
15
+ Existing v1 consumers should pin `pgserve@^1.x` in their `package.json` until
16
+ they have completed the migration described below. v2 changes the default
17
+ transport (Unix socket, no TCP), the identity model (kernel-rooted
18
+ fingerprint), the database layout (one DB per fingerprint), and the daemon
19
+ process model (singleton). A blind upgrade will break v1 connection strings.
20
+
21
+ ```jsonc
22
+ // package.json — keep v1 until you migrate
23
+ {
24
+ "dependencies": {
25
+ "pgserve": "^1.2.0"
26
+ }
27
+ }
28
+ ```
29
+
30
+ ### Breaking changes
31
+
32
+ - **TCP is no longer the default.** v1 bound `127.0.0.1:8432` for every
33
+ consumer. v2 binds a Unix control socket at
34
+ `${XDG_RUNTIME_DIR:-/tmp}/pgserve/control.sock` (mode `0600`, dir mode
35
+ `0700`) plus a `.s.PGSQL.5432` symlink so libpq clients connect with no
36
+ host/port/user/password. To keep a TCP listener, opt in explicitly with
37
+ `--listen <port>` (see "Compat TCP via --listen" in the README).
38
+ - **Fingerprint enforcement is default-ON.** Each connecting peer is
39
+ identified via `SO_PEERCRED` + the resolved `package.json` `name`,
40
+ collapsed to a 12-hex fingerprint. The daemon refuses to route a peer
41
+ into a database that does not match its fingerprint with SQLSTATE
42
+ `28P01 invalid_authorization — database fingerprint mismatch`. The
43
+ emergency kill switch is `PGSERVE_DISABLE_FINGERPRINT_ENFORCEMENT=1`
44
+ (deprecated; the daemon emits a stderr warning at boot when the env var
45
+ is observed).
46
+ - **Database-per-fingerprint isolation.** v1 served arbitrary database
47
+ names freely. v2 auto-creates `app_<sanitized-name>_<12hex>` for each
48
+ unique fingerprint on first connect; cross-fingerprint reads are denied.
49
+ `psql -l` will show one row per consumer rather than the shared pool
50
+ v1 produced. Monorepo rule: the root `package.json` `name` wins for all
51
+ packages under it.
52
+ - **Singleton daemon via control socket.** v1 spun up a server per
53
+ invocation, leaving consumers to coordinate ports themselves. v2
54
+ enforces one daemon per host: a second `pgserve daemon` exits with
55
+ `already running, pid N`. Run it under PM2 or systemd (snippets in the
56
+ README) — there is no PM-managed multi-process mode anymore.
57
+ - **GC sweep emits `db_reaped_ttl` and `db_reaped_liveness` audit events.**
58
+ Default lifecycle is now ephemeral: a database whose `liveness_pid` is
59
+ dead AND whose `last_connection_at` is older than 24h is dropped on the
60
+ next sweep (boot, hourly, sampled on-connect). To opt out, add
61
+ `pgserve.persist: true` to the consumer's `package.json` — flagged
62
+ databases are never reaped.
63
+
64
+ ### Migration guide
65
+
66
+ 1. **Connection strings** — drop credentials and the port; switch to the
67
+ socket form.
68
+
69
+ ```diff
70
+ - postgres://user:pass@localhost:5432/db
71
+ + postgres:///db?host=${XDG_RUNTIME_DIR:-/tmp}/pgserve
72
+ ```
73
+
74
+ Equivalently, for `psql`:
75
+
76
+ ```bash
77
+ psql -h "${XDG_RUNTIME_DIR:-/tmp}/pgserve" -d myapp
78
+ ```
79
+
80
+ 2. **Long-lived apps** — anything whose data needs to outlive a 24h idle
81
+ window (genie state stores, dashboards, anything with state worth
82
+ keeping) must declare persistence in its `package.json`:
83
+
84
+ ```jsonc
85
+ {
86
+ "name": "my-long-lived-app",
87
+ "pgserve": { "persist": true }
88
+ }
89
+ ```
90
+
91
+ Without this flag, the GC sweep will reap the database after the TTL
92
+ plus liveness check passes.
93
+
94
+ 3. **Need TCP?** Opt in with `--listen` and use issued tokens. TCP peers
95
+ cannot use `SO_PEERCRED`, so they must authenticate at connect time.
96
+
97
+ ```bash
98
+ pgserve daemon --listen :5432
99
+
100
+ # Issue a bearer token for a known fingerprint (printed once):
101
+ pgserve daemon issue-token --fingerprint <12hex>
102
+
103
+ # TCP clients pass the token via libpq application_name as
104
+ # ?fingerprint=<hex>&token=<bearer>
105
+ # Revoke when done:
106
+ pgserve daemon revoke-token <token-id>
107
+ ```
108
+
109
+ Without `--listen`, no TCP port is bound — verify with
110
+ `ss -tlnp | grep -v pgserve` returning no pgserve rows.
111
+
112
+ 4. **Kill switch (emergency only).** If the fingerprint enforcement
113
+ denies a connection you cannot otherwise unblock, set
114
+ `PGSERVE_DISABLE_FINGERPRINT_ENFORCEMENT=1` for the daemon. The
115
+ bypassed connection emits an `enforcement_kill_switch_used` audit
116
+ event; the daemon logs a deprecation warning at boot whenever the
117
+ variable is observed. The kill switch will be removed in a future
118
+ major; treat it as a debugging tool, not a production setting.
119
+
120
+ ### New features (group references map to wish execution groups)
121
+
122
+ - **Group 4 — Database-per-fingerprint + enforcement + kill switch.**
123
+ Auto-create `app_<name>_<12hex>` on first connect, deny
124
+ cross-fingerprint reads with SQLSTATE `28P01`, audit event
125
+ `connection_denied_fingerprint_mismatch`. Sanitizer collapses
126
+ non-`[a-z0-9]` runs to `_`, lowercases, truncates to 30 chars to keep
127
+ the resulting DB name ≤ 63 chars.
128
+ - **Group 5 — Lifecycle + persist flag + GC sweep.** Three-layer
129
+ lifecycle: liveness (peer pid alive), 24h TTL since last connection,
130
+ and `pgserve.persist: true` override. Sweep runs at daemon boot,
131
+ hourly, and sampled on-connect at 1/N where N = max(1, dbCount/10).
132
+ Reaped databases emit `db_reaped_ttl` or `db_reaped_liveness` audit
133
+ events; the on-connect sweep does not block accept latency past 50 ms
134
+ P99.
135
+ - **Group 6 — `--listen` opt-in TCP + token auth.** Daemon CLI accepts
136
+ `--listen [host:]port` (repeatable). Tokens issued via
137
+ `pgserve daemon issue-token --fingerprint <hex>`, hashed at rest into
138
+ `pgserve_meta.allowed_tokens`, verified with constant-time compare.
139
+ New audit events: `tcp_token_issued`, `tcp_token_used`,
140
+ `tcp_token_denied`. Without `--listen`, no TCP port is bound.
141
+
142
+ ### Compatibility
143
+
144
+ - Node.js >= 18 (unchanged).
145
+ - Linux x64, macOS ARM64/x64, Windows x64. Windows uses named pipes for
146
+ the control socket; PM2/systemd snippets are Linux-first.
147
+ - `--ram` (Linux/WSL2 `/dev/shm`), `--pgvector`, `--sync-to`, and the
148
+ rest of the v1 runtime flags continue to work unchanged.
149
+
150
+ ---
package/Makefile CHANGED
@@ -79,13 +79,13 @@ bench: ## Run benchmarks
79
79
  .PHONY: test-local
80
80
  test-local: ## Test server locally
81
81
  @echo "$(CYAN)🧪 Testing server...$(RESET)"
82
- @./bin/pglite-server.js start ./data/test-local --port 12050 --log info &
82
+ @./bin/postgres-server.js start ./data/test-local --port 12050 --log info &
83
83
  @TESTPID=$$!; \
84
84
  sleep 3; \
85
- ./bin/pglite-server.js list; \
86
- ./bin/pglite-server.js health --port 12050; \
85
+ ./bin/postgres-server.js list; \
86
+ ./bin/postgres-server.js health --port 12050; \
87
87
  kill $$TESTPID 2>/dev/null || true; \
88
- ./bin/pglite-server.js cleanup
88
+ ./bin/postgres-server.js cleanup
89
89
  @echo "$(GREEN)✅ Server test passed!$(RESET)"
90
90
 
91
91
  # ==========================================
@@ -152,7 +152,7 @@ check-version: ## Check if version tag exists
152
152
 
153
153
  check-files: ## Check required files exist
154
154
  @echo "$(CYAN)🔍 Checking required files...$(RESET)"
155
- @for file in package.json README.md LICENSE src/index.js bin/pglite-server.js; do \
155
+ @for file in package.json README.md LICENSE src/index.js bin/postgres-server.js; do \
156
156
  if [ ! -f "$$file" ]; then \
157
157
  echo "$(RED)❌ Missing required file: $$file$(RESET)"; \
158
158
  exit 1; \
@@ -172,26 +172,26 @@ $(DIST_DIR):
172
172
 
173
173
  build: $(DIST_DIR) ## Build standalone executable for current platform
174
174
  @echo "$(CYAN)🔨 Building standalone executable...$(RESET)"
175
- @bun build --compile bin/pglite-server.js --outfile $(DIST_DIR)/pgserve
175
+ @bun build --compile bin/postgres-server.js --outfile $(DIST_DIR)/pgserve
176
176
  @echo "$(GREEN)✅ Built: $(DIST_DIR)/pgserve$(RESET)"
177
177
 
178
178
  build-linux: $(DIST_DIR) ## Build for Linux (x64 + arm64)
179
179
  @echo "$(CYAN)🐧 Building for Linux...$(RESET)"
180
- @bun build --compile --target=bun-linux-x64 bin/pglite-server.js --outfile $(DIST_DIR)/pgserve-linux-x64
181
- @bun build --compile --target=bun-linux-arm64 bin/pglite-server.js --outfile $(DIST_DIR)/pgserve-linux-arm64
180
+ @bun build --compile --target=bun-linux-x64 bin/postgres-server.js --outfile $(DIST_DIR)/pgserve-linux-x64
181
+ @bun build --compile --target=bun-linux-arm64 bin/postgres-server.js --outfile $(DIST_DIR)/pgserve-linux-arm64
182
182
  @echo "$(GREEN)✅ Built: $(DIST_DIR)/pgserve-linux-x64$(RESET)"
183
183
  @echo "$(GREEN)✅ Built: $(DIST_DIR)/pgserve-linux-arm64$(RESET)"
184
184
 
185
185
  build-macos: $(DIST_DIR) ## Build for macOS (x64 + arm64)
186
186
  @echo "$(CYAN)🍎 Building for macOS...$(RESET)"
187
- @bun build --compile --target=bun-darwin-x64 bin/pglite-server.js --outfile $(DIST_DIR)/pgserve-darwin-x64
188
- @bun build --compile --target=bun-darwin-arm64 bin/pglite-server.js --outfile $(DIST_DIR)/pgserve-darwin-arm64
187
+ @bun build --compile --target=bun-darwin-x64 bin/postgres-server.js --outfile $(DIST_DIR)/pgserve-darwin-x64
188
+ @bun build --compile --target=bun-darwin-arm64 bin/postgres-server.js --outfile $(DIST_DIR)/pgserve-darwin-arm64
189
189
  @echo "$(GREEN)✅ Built: $(DIST_DIR)/pgserve-darwin-x64$(RESET)"
190
190
  @echo "$(GREEN)✅ Built: $(DIST_DIR)/pgserve-darwin-arm64$(RESET)"
191
191
 
192
192
  build-windows: $(DIST_DIR) ## Build for Windows (x64)
193
193
  @echo "$(CYAN)🪟 Building for Windows...$(RESET)"
194
- @bun build --compile --target=bun-windows-x64 bin/pglite-server.js --outfile $(DIST_DIR)/pgserve-windows-x64.exe
194
+ @bun build --compile --target=bun-windows-x64 bin/postgres-server.js --outfile $(DIST_DIR)/pgserve-windows-x64.exe
195
195
  @echo "$(GREEN)✅ Built: $(DIST_DIR)/pgserve-windows-x64.exe$(RESET)"
196
196
 
197
197
  build-all: build-linux build-macos build-windows ## Build for all platforms
@@ -254,7 +254,7 @@ publish: ## ⚠️ [DEPRECATED] Releases run from CI on push to main
254
254
  clean: ## Clean generated files
255
255
  @echo "$(CYAN)🧹 Cleaning...$(RESET)"
256
256
  @rm -rf data/test-* data/genieos-local
257
- @./bin/pglite-server.js cleanup
257
+ @./bin/postgres-server.js cleanup
258
258
  @echo "$(GREEN)✅ Cleaned!$(RESET)"
259
259
 
260
260
  clean-all: clean ## Deep clean (including node_modules)
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  <a href="https://discord.gg/xcW8c7fF3R"><img src="https://img.shields.io/discord/1095114867012292758?style=flat-square&color=00D9FF&label=discord" alt="Discord"></a>
11
11
  </p>
12
12
 
13
- <p><em>Zero config, auto-provision databases, unlimited concurrent connections. Just works.</em></p>
13
+ <p><em>npx pgserve and it just works, no credentials needed. Zero config, auto-provision databases, unlimited concurrent connections.</em></p>
14
14
 
15
15
  <p>
16
16
  <a href="#-quick-start">Quick Start</a> •
@@ -35,6 +35,8 @@ Connect from any PostgreSQL client — databases auto-create on first connection
35
35
  psql postgresql://localhost:8432/myapp
36
36
  ```
37
37
 
38
+ > Note: v2 default is the Unix socket — see [Daemon mode](#daemon-mode). The TCP form above is the v1 compat path.
39
+
38
40
  <br>
39
41
 
40
42
  ## Features
@@ -168,8 +170,212 @@ pgserve --sync-to "postgresql://user:pass@db.example.com:5432/prod"
168
170
 
169
171
  <br>
170
172
 
173
+ ## Daemon mode
174
+
175
+ `pgserve@2` ships a singleton daemon that binds a Unix control socket
176
+ inside `$XDG_RUNTIME_DIR/pgserve` (fallback `/tmp/pgserve`). One daemon
177
+ per host serves every consumer on the box — no port conflicts, no
178
+ credentials, kernel-rooted identity. Run it under PM2 or systemd so it
179
+ restarts automatically.
180
+
181
+ ```bash
182
+ # Foreground (for debugging)
183
+ pgserve daemon
184
+
185
+ # Stop a running daemon
186
+ pgserve daemon stop
187
+ ```
188
+
189
+ A second `pgserve daemon` invocation while the first is running exits with
190
+ `already running, pid N`. A daemon killed with `kill -9` leaves an orphan
191
+ PID file + socket; the next `pgserve daemon` boot detects the dead pid and
192
+ cleans both up automatically.
193
+
194
+ Connect from any libpq client (no host/port/user/password required —
195
+ the daemon authenticates via SO_PEERCRED on accept):
196
+
197
+ ```bash
198
+ psql -h "${XDG_RUNTIME_DIR:-/tmp}/pgserve" -d myapp
199
+ # or via connection URI
200
+ psql "postgresql:///myapp?host=${XDG_RUNTIME_DIR:-/tmp}/pgserve"
201
+ ```
202
+
203
+ ### Supervised by PM2
204
+
205
+ `ecosystem.config.cjs` snippet:
206
+
207
+ ```javascript
208
+ module.exports = {
209
+ apps: [{
210
+ name: 'pgserve',
211
+ script: 'pgserve',
212
+ args: 'daemon',
213
+ autorestart: true,
214
+ max_memory_restart: '1G',
215
+ env: { XDG_RUNTIME_DIR: '/run/user/1000' },
216
+ }],
217
+ };
218
+ ```
219
+
220
+ ```bash
221
+ pm2 start ecosystem.config.cjs && pm2 save
222
+ ```
223
+
224
+ ### Supervised by systemd
225
+
226
+ `/etc/systemd/user/pgserve.service`:
227
+
228
+ ```ini
229
+ [Unit]
230
+ Description=pgserve daemon
231
+ After=default.target
232
+
233
+ [Service]
234
+ Type=simple
235
+ ExecStart=/usr/bin/env npx pgserve daemon
236
+ Restart=on-failure
237
+ RestartSec=5
238
+
239
+ [Install]
240
+ WantedBy=default.target
241
+ ```
242
+
243
+ Enable for the current user:
244
+
245
+ ```bash
246
+ systemctl --user enable --now pgserve
247
+ journalctl --user -u pgserve -f
248
+ ```
249
+
250
+ The systemd user unit inherits `XDG_RUNTIME_DIR` automatically; the daemon
251
+ binds `${XDG_RUNTIME_DIR}/pgserve/control.sock` (mode 0600, dir mode 0700)
252
+ plus a `.s.PGSQL.5432` symlink so off-the-shelf PostgreSQL clients connect
253
+ without further configuration.
254
+
255
+ <br>
256
+
257
+ ## Fingerprint isolation
258
+
259
+ Each consumer is identified by a **kernel-rooted fingerprint** derived from
260
+ the peer's `SO_PEERCRED` plus the resolved `package.json` `name`, collapsed
261
+ to 12 hex chars. The daemon auto-creates one database per fingerprint —
262
+ `app_<sanitized-name>_<12hex>` — and refuses to route a peer into any other
263
+ database with SQLSTATE `28P01 invalid_authorization — database fingerprint
264
+ mismatch`.
265
+
266
+ ```bash
267
+ # What `psql -l` shows on a host with three consumers:
268
+ $ psql -h "${XDG_RUNTIME_DIR:-/tmp}/pgserve" -l
269
+ Name | Owner | ...
270
+ -----------------------+----------+----
271
+ app_genie_a1b2c3d4e5f6 | postgres | ...
272
+ app_brain_4f3e2d1c0b9a | postgres | ...
273
+ app_omni_9876543210ab | postgres | ...
274
+ ```
275
+
276
+ **Monorepo rule:** the **root** `package.json` `name` wins. Every workspace
277
+ under it shares one fingerprint and one database — sub-packages do **not**
278
+ get their own. If you need separate isolation, run them from separate
279
+ checkouts.
280
+
281
+ **Sanitization:** non-`[a-z0-9]` runs collapse to `_`, lowercased, truncated
282
+ to 30 chars so the final DB name stays within PostgreSQL's 63-char limit.
283
+ A name like `@scope/foo bar` becomes `_scope_foo_bar`.
284
+
285
+ **Emergency kill switch:** `PGSERVE_DISABLE_FINGERPRINT_ENFORCEMENT=1`
286
+ disables enforcement for the daemon process. Use it as a debugging tool
287
+ only — every bypassed connection emits an `enforcement_kill_switch_used`
288
+ audit event and the daemon logs a deprecation warning at boot.
289
+
290
+ <br>
291
+
292
+ ## Long-running apps: `pgserve.persist`
293
+
294
+ Default lifecycle is **ephemeral**: a database whose `liveness_pid` is dead
295
+ AND whose `last_connection_at` is older than 24h is dropped on the next GC
296
+ sweep (boot, hourly, sampled on-connect). Reaped DBs emit
297
+ `db_reaped_ttl` or `db_reaped_liveness` audit events.
298
+
299
+ If your app holds state worth keeping past 24h of idle — genie's wish/agent
300
+ store, internal dashboards, anything you'd be unhappy to lose — declare
301
+ persistence in `package.json`:
302
+
303
+ ```jsonc
304
+ {
305
+ "name": "my-long-lived-app",
306
+ "pgserve": { "persist": true }
307
+ }
308
+ ```
309
+
310
+ Persisted databases are **never** reaped, regardless of liveness or TTL.
311
+ Dev workloads with long debug cycles do not normally need this — any new
312
+ connection slides the TTL window forward. Reach for `pgserve.persist` when
313
+ the app is genuinely long-lived (production daemon, dashboard, durable
314
+ agent state), not just for convenience.
315
+
316
+ <br>
317
+
318
+ ## Compat TCP via `--listen`
319
+
320
+ TCP is **off by default** in v2. Bring it back only when you need it
321
+ (Kubernetes pods, remote sync, legacy clients that cannot speak Unix
322
+ sockets) by opting in:
323
+
324
+ ```bash
325
+ pgserve daemon --listen :5432
326
+ # Repeatable for multiple binds:
327
+ pgserve daemon --listen :5432 --listen 0.0.0.0:5433
328
+ ```
329
+
330
+ TCP peers cannot use `SO_PEERCRED`, so they **must** authenticate at
331
+ connect time. Issue a bearer token bound to a known fingerprint:
332
+
333
+ ```bash
334
+ # Prints the token ONCE; the daemon stores only its hash.
335
+ pgserve daemon issue-token --fingerprint a1b2c3d4e5f6
336
+
337
+ # TCP client passes it via libpq application_name:
338
+ # ?fingerprint=a1b2c3d4e5f6&token=<bearer>
339
+
340
+ # Revoke when done:
341
+ pgserve daemon revoke-token <token-id>
342
+ ```
343
+
344
+ Audit events: `tcp_token_issued`, `tcp_token_used`, `tcp_token_denied`.
345
+ Tokens are verified with constant-time compare. Without a valid token a
346
+ TCP connection is refused — there is no anonymous TCP path.
347
+
348
+ Verify no port is bound when `--listen` is **not** set:
349
+
350
+ ```bash
351
+ ss -tlnp | grep pgserve # no rows expected
352
+ ```
353
+
354
+ <br>
355
+
171
356
  ## API
172
357
 
358
+ Daemon-first apps can let the first caller install/start the singleton and
359
+ then connect through the Unix socket. The daemon derives the app identity
360
+ from kernel peer credentials and routes it to that app's signed fingerprint
361
+ database.
362
+
363
+ ```javascript
364
+ import { daemonClientOptions, ensureDaemon } from 'pgserve';
365
+ import postgres from 'postgres';
366
+
367
+ await ensureDaemon({
368
+ dataDir: `${process.env.HOME}/.pgserve/data`,
369
+ logLevel: 'warn',
370
+ });
371
+
372
+ const sql = postgres(daemonClientOptions());
373
+ await sql`SELECT current_database()`;
374
+ ```
375
+
376
+ The classic TCP router API remains available for explicit v1-compatible
377
+ embedded servers:
378
+
173
379
  ```javascript
174
380
  import { startMultiTenantServer } from 'pgserve';
175
381
 
@@ -325,10 +531,10 @@ CREATE EXTENSION IF NOT EXISTS vector;
325
531
  <tr>
326
532
  <th>Scenario</th>
327
533
  <th>SQLite</th>
328
- <th>PGlite</th>
329
534
  <th>PostgreSQL</th>
330
- <th>pgserve</th>
331
- <th>pgserve --ram</th>
535
+ <th>pgserve 1.2.0</th>
536
+ <th>pgserve v2</th>
537
+ <th>pgserve v2 --ram</th>
332
538
  </tr>
333
539
  <tr>
334
540
  <td><b>Concurrent Writes</b> (10 agents)</td>
@@ -361,10 +567,10 @@ CREATE EXTENSION IF NOT EXISTS vector;
361
567
  <table>
362
568
  <tr>
363
569
  <th>Metric</th>
364
- <th>PGlite</th>
365
570
  <th>PostgreSQL</th>
366
- <th>pgserve</th>
367
- <th>pgserve --ram</th>
571
+ <th>pgserve 1.2.0</th>
572
+ <th>pgserve v2</th>
573
+ <th>pgserve v2 --ram</th>
368
574
  </tr>
369
575
  <tr>
370
576
  <td><b>Vector INSERT</b> (1000 × 1536-dim)</td>
@@ -413,7 +619,7 @@ CREATE EXTENSION IF NOT EXISTS vector;
413
619
  <td>117</td>
414
620
  </tr>
415
621
  <tr>
416
- <td>PGlite</td>
622
+ <td>pgserve 1.2.0</td>
417
623
  <td>305</td>
418
624
  <td>65</td>
419
625
  <td>100%</td>
@@ -431,7 +637,7 @@ CREATE EXTENSION IF NOT EXISTS vector;
431
637
  <td>1,067</td>
432
638
  </tr>
433
639
  <tr>
434
- <td>pgserve</td>
640
+ <td>pgserve v2</td>
435
641
  <td>2,145</td>
436
642
  <td>149</td>
437
643
  <td>100%</td>
@@ -440,7 +646,7 @@ CREATE EXTENSION IF NOT EXISTS vector;
440
646
  <td>1,347</td>
441
647
  </tr>
442
648
  <tr>
443
- <td><b>pgserve --ram</b></td>
649
+ <td><b>pgserve v2 --ram</b></td>
444
650
  <td><b>3,541</b></td>
445
651
  <td><b>381</b></td>
446
652
  <td><b>100%</b></td>
@@ -61,19 +61,19 @@ if (!bunPath) {
61
61
  // then exits instantly with:
62
62
  // Error: Bun's postinstall script was not run.
63
63
  //
64
- // pglite-server.js's TCP readiness poll can't distinguish this from a slow
64
+ // postgres-server.js's TCP readiness poll can't distinguish this from a slow
65
65
  // startup, so users see a confusing 30s timeout. Detect the specific error
66
66
  // here, attempt the documented self-heal once (`node install.js`), and retry.
67
67
  // If self-heal also fails, surface the real error instead of hanging later.
68
68
  ensureBunHealthy(bunPath);
69
69
 
70
- const scriptPath = path.join(__dirname, 'pglite-server.js');
70
+ const scriptPath = path.join(__dirname, 'postgres-server.js');
71
71
 
72
72
  /**
73
73
  * Verify the selected bun binary can execute. If it fails with the known
74
74
  * "postinstall script was not run" signature, attempt a one-shot repair via
75
75
  * the bun npm package's install.js. Throws (with a useful message) rather
76
- * than letting pglite-server.js hang on the TCP readiness poll for 30s.
76
+ * than letting postgres-server.js hang on the TCP readiness poll for 30s.
77
77
  */
78
78
  function ensureBunHealthy(bunExe) {
79
79
  const probe = probeBun(bunExe);