Skip to content

feat(api): move from Turso/libSQL to Postgres via @profullstack/libsql-pg - #130

Merged
ralyodio merged 2 commits into
mainfrom
feat/postgres
Sep 25, 2026
Merged

ralyodio merged 2 commits into
mainfrom
feat/postgres

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

What changed

The api service's database layer moves from Turso/libSQL to Postgres at the code level. Nothing here touches a server or deploys anything.

There was no partial Postgres path to finish: nothing in the repo imported pg or read DATABASE_URL (the pg mention was mistaken), so the port uses the house shim.

  • services/api/src/db.ts builds the client with @profullstack/libsql-pg@0.1.1 (createClient({ url })). It keeps the @libsql/client surface over a pg pool and rewrites SQLite idioms (datetime('now'), ON CONFLICT, ? placeholders) per statement, so the queries in db.ts, store/db.ts and push/routes.ts are unchanged apart from the two hand ports below.
  • Fail fast: DATABASE_URL (or the alias TRONBROWSER_DB_URL) must be postgres:///postgresql://. Missing, libsql://, file: or a set TRONBROWSER_DB_PATH throws a clear error; assertDatabaseUrl() runs at start in services/api/src/index.ts. No fallback to a file database.
  • Postgres migrations in packages/storage/migrations-pg/ (7 files), generated with npx libsql-pg convert-schema and reviewed. The converter promoted created_at/updated_at/last_seen_at (text with a datetime('now') default) to timestamptz; I also made expires_at (sessions, email_tokens), paid_at, scanned_at, provisioned_at, last_used_at and deleted_at timestamptz, because the app stamps them with datetime('now') (now now()) or compares them against it. push_messages.expires_at stays a unix-seconds bigint; 0/1 flags (enabled, email_verified) stay bigint.
  • Runner: scripts/db-migrate.mjs applies migrations-pg (one transaction per file, recorded in schema_migrations) when the URL is Postgres; a libsql:///file: URL still applies the SQLite migrations/ so the old database can be kept in step until the cutover is proven. @libsql/client moved from dependencies to devDependencies (that path and the in-memory tests only).
  • .env.example and packages/storage/README.md updated. packages/storage/src/config.ts (the unused Turso/libsql config resolver and its tests) is left as is.

Idioms fixed by hand

  • createSession / putEmailToken: datetime('now', ?) with a bound modifier ('+3600 seconds') is not rewritten by libsql-pg (the modifier must be a literal), and Postgres has no datetime(). The expiry is now computed in JS (expiresAt(ttl)) and bound as an ISO string into the timestamptz column.
  • listLiveExtensions: LIKE became ILIKE. SQLite's LIKE ignores ASCII case and store search relied on that; Postgres's LIKE is case-sensitive.
  • No rowid, ?NNN, json_each, COLLATE NOCASE, last_insert_rowid(), LIMIT -1, FTS5, GLOB or IS NOT <value> in this service.

Tests

pnpm build, pnpm typecheck and pnpm test pass with no database (services/api: 14 files, 96 tests). push.test.ts runs the whole push protocol suite against in-memory SQLite by default; with TEST_DATABASE_URL=postgres://... it applies migrations-pg/0007_push.sql and runs the same requests through the shim. db.test.ts (new) covers the URL guard and expiry helper.

Cutover recipe (coordinator)

# 1. schema
DATABASE_URL=postgres://... pnpm db:migrate
# 2. rows (+ verify counts per table)
npx libsql-pg copy --from "$TRONBROWSER_DB_URL" --token "$TRONBROWSER_DB_AUTH_TOKEN" --to "$DATABASE_URL" --verify
# 3. set DATABASE_URL in Doppler/the vault, unset TRONBROWSER_DB_URL/_AUTH_TOKEN/_PATH, deploy

Beyond the recipe: the copier coerces the ISO-text timestamps into the timestamptz columns using information_schema; check sessions.expires_at and email_tokens.expires_at landed (select count(*) from sessions where expires_at > now()). All primary keys are text, so there are no sequences to reset. Run ANALYZE after the load.

🤖 Generated with Claude Code

…l-pg

The api service's db() now builds its client with @profullstack/libsql-pg,
which keeps the @libsql/client surface (execute / rows / rowsAffected) over a
pg pool and rewrites the SQLite idioms in the existing queries per statement,
so the call sites in db.ts, store/db.ts and push/routes.ts did not change.

- DATABASE_URL (postgres://; TRONBROWSER_DB_URL accepted as an alias) is the
  only accepted setting. Anything else throws a clear error, at process start
  too (assertDatabaseUrl in index.ts). No fallback to a file database.
- Postgres migrations in packages/storage/migrations-pg, generated with
  `npx libsql-pg convert-schema` and reviewed: the text columns the app stamps
  with datetime('now') or compares against it (created_at, updated_at,
  expires_at, paid_at, scanned_at, provisioned_at, last_used_at, deleted_at,
  last_seen_at) are timestamptz; push_messages.expires_at stays a unix
  bigint; 0/1 flags stay bigint.
- scripts/db-migrate.mjs applies migrations-pg (one transaction per file)
  when the URL is postgres://, and still applies the SQLite files for a
  libsql:// or file: URL so the old database can be kept in step until the
  cutover is proven. @libsql/client is now a devDependency only (that legacy
  path and the in-memory push tests).
- Hand ports: createSession/putEmailToken bound `datetime('now', ?)` with a
  bound modifier, which the rewriter cannot turn into an interval; the expiry
  is computed in JS and bound. Store search uses ILIKE, since SQLite's LIKE
  ignored ASCII case and Postgres's does not.
- push.test.ts runs on in-memory SQLite by default and against
  migrations-pg through the shim when TEST_DATABASE_URL is set; db.test.ts
  covers the URL guard without a database.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@socket-security

socket-security Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​profullstack/​libsql-pg@​0.1.17710010088100

View full report

Comment thread .env.example Fixed
Comment thread services/api/src/db.test.ts Fixed
Comment thread services/api/src/db.test.ts Fixed
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

ThreatCrush Security Scan

52 finding(s)

HIGH/CRITICAL: 3 | MEDIUM: 28 | LOW: 21

Severity Rule Location
HIGH py-ssrf-outbound-request apps/desktop/launcher/tron-tor-helper:303
HIGH py-ssrf-outbound-request apps/desktop/launcher/tron-tor-helper:437
HIGH sh-remote-script-execution apps/web/public/install.sh:879
MEDIUM js-open-redirect apps/desktop/extensions/ai-sidebar/install-helper.js:156
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:34
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/media.js:57
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:237
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:266
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/newtab.js:336
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/options.js:306
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/sidepanel.js:78
MEDIUM js-unescaped-html-sink apps/desktop/extensions/ai-sidebar/sidepanel.js:166
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:77
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:227
MEDIUM js-unescaped-html-sink apps/extensions/public/store.js:569
MEDIUM js-unescaped-html-sink apps/web/public/app.js:29
MEDIUM js-unescaped-html-sink apps/web/public/dns.js:54
MEDIUM sh-remote-script-execution apps/web/public/install.sh:164
MEDIUM sh-remote-script-execution apps/web/public/install.sh:169
MEDIUM sh-remote-script-execution apps/web/public/install.sh:404
MEDIUM sh-remote-script-execution apps/web/public/install.sh:422
MEDIUM sh-remote-script-execution apps/web/public/install.sh:935
MEDIUM sh-remote-script-execution apps/web/public/install.sh:986
MEDIUM js-unescaped-html-sink apps/web/public/settings.js:96
MEDIUM js-unescaped-html-sink apps/web/public/settings.js:168
MEDIUM js-dynamic-code-execution packages/sdk/src/mcp/tools.ts:125
MEDIUM js-dynamic-code-execution packages/sdk/src/page.ts:68
MEDIUM js-dynamic-code-execution packages/sdk/src/page.ts:73
MEDIUM sql-template-interpolation services/api/src/store/db.ts:116
MEDIUM js-dynamic-code-execution services/api/src/store/scanner.ts:44
MEDIUM sh-predictable-temp-path start.sh:25
LOW js-dynamic-code-execution apps/desktop/extensions/ai-sidebar/pit-proxy.test.js:8
LOW js-dynamic-code-execution packages/agent-runtime/src/analyze/form-script.test.ts:6
LOW js-unescaped-html-sink packages/agent-runtime/src/analyze/form-script.test.ts:26
LOW js-unescaped-html-sink packages/agent-runtime/src/analyze/form-script.test.ts:49
LOW js-dynamic-code-execution packages/browser-core/src/automation/extract-script.test.ts:6
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:16
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:45
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:57
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:80
LOW js-unescaped-html-sink packages/browser-core/src/automation/extract-script.test.ts:99
LOW js-dynamic-code-execution packages/browser-core/src/automation/snapshot-script.test.ts:11
LOW js-unescaped-html-sink packages/browser-core/src/automation/snapshot-script.test.ts:62
LOW js-unescaped-html-sink packages/browser-core/src/automation/snapshot-script.test.ts:101
LOW js-unescaped-html-sink packages/provenance/src/scan.test.ts:47
LOW js-unescaped-html-sink packages/provenance/src/scan.test.ts:170
LOW js-unescaped-html-sink packages/provenance/src/scan.test.ts:216
LOW js-dynamic-code-execution packages/sdk/src/mcp/automate.test.ts:261
LOW secret-generic-api-key packages/storage/src/config.ts:51
LOW secret-generic-credential packages/storage/src/config.ts:51

…and 2 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

ThreatCrush flags any user:password@ database URL, placeholder or not. The
example and test URLs now carry no password; the real one lives in the vault.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ralyodio
ralyodio merged commit e5504a9 into main Sep 25, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants