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.
- package/.genie/brainstorms/pgserve-v2/DESIGN.md +174 -0
- package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +99 -0
- package/.genie/wishes/pgserve-v2/WISH.md +442 -0
- package/.genie/wishes/release-system-genie-pattern/WISH.md +9 -9
- package/.genie/wishes/release-system-genie-pattern/validation.md +43 -10
- package/.github/workflows/ci.yml +10 -6
- package/.github/workflows/release.yml +1 -1
- package/.github/workflows/version.yml +4 -4
- package/CHANGELOG.md +150 -0
- package/Makefile +12 -12
- package/README.md +216 -10
- package/bin/pgserve-wrapper.cjs +3 -3
- package/bin/{pglite-server.js → postgres-server.js} +258 -1
- package/bun.lock +0 -3
- package/ecosystem.config.cjs +3 -3
- package/eslint.config.js +2 -0
- package/knip.json +1 -1
- package/package.json +4 -5
- package/scripts/test-bun-self-heal.sh +10 -10
- package/src/admin-client.js +171 -0
- package/src/audit.js +168 -0
- package/src/control-db.js +313 -0
- package/src/daemon-control.js +408 -0
- package/src/daemon-shared.js +18 -0
- package/src/daemon-tcp.js +296 -0
- package/src/daemon.js +629 -0
- package/src/fingerprint.js +453 -0
- package/src/gc.js +351 -0
- package/src/index.js +31 -0
- package/src/protocol.js +131 -0
- package/src/router.js +8 -0
- package/src/sdk.js +137 -0
- package/src/tenancy.js +75 -0
- package/src/tokens.js +102 -0
- package/tests/audit.test.js +189 -0
- package/tests/benchmarks/runner.js +430 -754
- package/tests/control-db.test.js +285 -0
- package/tests/daemon-fingerprint-integration.test.js +111 -0
- package/tests/daemon-pr24-regression.test.js +198 -0
- package/tests/fingerprint.test.js +249 -0
- package/tests/fixtures/240-orphan-seed.sql +30 -0
- package/tests/orphan-cleanup.test.js +390 -0
- package/tests/sdk.test.js +71 -0
- package/tests/tcp-listen.test.js +368 -0
- 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/
|
|
82
|
+
@./bin/postgres-server.js start ./data/test-local --port 12050 --log info &
|
|
83
83
|
@TESTPID=$$!; \
|
|
84
84
|
sleep 3; \
|
|
85
|
-
./bin/
|
|
86
|
-
./bin/
|
|
85
|
+
./bin/postgres-server.js list; \
|
|
86
|
+
./bin/postgres-server.js health --port 12050; \
|
|
87
87
|
kill $$TESTPID 2>/dev/null || true; \
|
|
88
|
-
./bin/
|
|
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/
|
|
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/
|
|
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/
|
|
181
|
-
@bun build --compile --target=bun-linux-arm64 bin/
|
|
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/
|
|
188
|
-
@bun build --compile --target=bun-darwin-arm64 bin/
|
|
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/
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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>
|
|
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>
|
package/bin/pgserve-wrapper.cjs
CHANGED
|
@@ -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
|
-
//
|
|
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, '
|
|
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
|
|
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);
|