Skip to content

feat(pwa): move app.moshcode.sh from Turso/libSQL to Postgres via @profullstack/libsql-pg - #551

Merged
ralyodio merged 4 commits into
mainfrom
port/pwa-libsql-pg
Sep 25, 2026
Merged

ralyodio merged 4 commits into
mainfrom
port/pwa-libsql-pg

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

What changed

app.moshcode.sh (apps/pwa) moves from Turso/libSQL to Postgres at the code level. Nothing here touches a server or deploys anything.

  • src/db.mjs opens @profullstack/libsql-pg@0.1.2 for postgres:// URLs (createClient({ url, dialect: 'sqlite' })): the @libsql/client surface over a pg pool, with the SQLite idioms this code writes rewritten per statement — so no caller changed. A file: URL still opens @libsql/client (development, the whole test suite). Fail fast: a libsql:// URL throws at import with a message naming DATABASE_URL and DATABASE_AUTH_TOKEN; there is no fallback to Turso.
  • src/migrations-pg/ is the Postgres twin of src/migrations/ — 25 files, identical names, generated with npx libsql-pg convert-schema and reviewed. INTEGER PRIMARY KEY AUTOINCREMENT → identity (session_output.id, moshpit_tld_log.seq); every other INTEGER stays bigint (0/1 flags and millisecond timestamps, as the app writes them); REAL → double precision; partial indexes, CHECKs and the two views kept. Hand fixes: the converter emits DEFAULT 'x' (CHECK) (…) for DEFAULT 'x' CHECK (…) (010, 017, 023), and the team_access view's derived table needed an alias.
  • src/migrate.mjs picks the directory by dialect and sends the Postgres files verbatim on a dialect: 'postgres' client (the SQLite rewriter's schema converter is not idempotent on Postgres DDL). The _migrations ledger is keyed by file name, so a database whose ledger is copied across is already up to date.
  • Root Dockerfile (the one ops/dev2-deploy already runs with) installs apps/pwa's own dependencies with npm ci --omit=dev; both package-lock.json and pnpm-lock.yaml in apps/pwa carry the new dependency. @libsql/client stays, for file:.
  • README + .env.example updated; DATABASE_AUTH_TOKEN is no longer read.

Idioms fixed by hand

  • src/moshpit.mjs (listTldsNotOwnedBy, countTldsNotOwnedBy, searchScope): user_id IS NOT ? → user_id IS DISTINCT FROM ? (Postgres only has IS NOT NULL/TRUE/FALSE; SQLite ≥ 3.39 accepts both).
  • src/routes/sessions.mjs: a real Postgres bug the suite caught. session_output.seq is MAX(seq)+1 inside the INSERT — atomic on SQLite's single writer, not under READ COMMITTED with a connection per request: a burst of five chunks came back [1,1,2,3,3], and a browser resuming from ?since= skips the repeat for good. On Postgres the owning cli_sessions row is now locked FOR UPDATE in a short transaction before the insert (SQLite path unchanged); migration 025 makes (session_id, seq) unique as the backstop. The live Turso data has 0 duplicate pairs.
  • Everything else the rewriter handles at run time: INSERT OR IGNORE / INSERT OR REPLACE (moshpit_alias_exempt, moshpit_tlds, moshpit_leases, mcp_oauth_grants, organization_members, session_team_shares), PRAGMA (no-op), strftime. Probed against Postgres 17: INSERT … SELECT ?,… WHERE (SELECT SUM(delta)…) >= ? (credits), COALESCE(?, col) updates, ORDER BY price_usd IS NULL … LIMIT ? OFFSET ?, db.batch, db.transaction + tx.close(), rowsAffected on ON CONFLICT DO NOTHING. No rowid, json_each, COLLATE NOCASE, GLOB, last_insert_rowid(), ?NNN, LIMIT -1, FTS, printf/typeof in this app. LIKE inputs are lowercased before they reach SQL (lib/moshpit-search.mjs), so Postgres's case-sensitive LIKE matches the same rows.

Tests

  • SQLite (unchanged path): node --test in apps/pwa — 777 pass, 0 fail (775 on main + 2 new guards in sql-portability: the two migration directories must mirror each other; no IS NOT ?).
  • Postgres: PG_TEST_ADMIN_URL=postgres://… node --import ./scripts/pg-test-preload.mjs --test runs the same suite on a real Postgres 17 (a throwaway database per test process, config.db.url redirected by a loader hook) — 775 pass, 0 fail, 2 skipped (the raw SQLite migration replay in mcp-migration, the PRAGMA foreign_keys probe). Before the fixes above it was 759/18.
  • Repo root node --test: 3512 pass, 0 fail, 4 skipped (baseline 3510/0/4).
  • Fixtures made dialect-neutral: ORDER BY rowid dropped where created_at is already distinct; the device-code seed clears its stale row instead of relying on INSERT OR REPLACE INTO users cascading (an upsert on Postgres, a delete+insert on SQLite).
  • Boot-migrate against an empty Postgres: 43 tables, 2 views, 28 ledger rows; a second run applies nothing. Table/column set diffed against the live Turso schema: identical (339 columns).

Cutover recipe (coordinator)

# 1. schema (boot-migrate applies src/migrations-pg/, including the _migrations ledger)
DATABASE_URL='postgres://…?sslmode=require' node apps/pwa/src/migrate.mjs
# 2. rows (+ verify counts per table); the ledger is already populated, leave it out
npx libsql-pg copy --from "$TURSO_URL" --token "$TURSO_TOKEN" --to "$DATABASE_URL" --exclude _migrations --verify
# 3. DATABASE_URL=postgres://… in app.env, drop DATABASE_AUTH_TOKEN, deploy main

Identity sequences (session_output.id max 5,205,921; moshpit_tld_log.seq max 19,539) are reset by the copier's setval. Run ANALYZE after the load.

🤖 Generated with Claude Code

ralyodio and others added 3 commits September 25, 2026 16:35
postgres:// URLs open @profullstack/libsql-pg (dialect 'sqlite', so the
statements this app writes are rewritten per call); file: keeps @libsql/client
for development and tests; libsql:// (Turso) is refused at boot.

src/migrations-pg/ is the Postgres twin of src/migrations/, same file names,
converted with `libsql-pg convert-schema` and reviewed (DEFAULT+CHECK order,
the derived-table alias in the team_access view). migrate.mjs picks the set by
dialect and sends the Postgres files verbatim on a dialect:'postgres' client,
because the SQLite rewriter's converter is not idempotent on Postgres DDL.

The root Dockerfile (from ops/dev2-deploy) installs apps/pwa's own dependencies;
the .nixpacks image installed only the CLI's.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…q under a row lock

`user_id IS NOT ?` is SQLite-only (Postgres has IS NOT NULL/TRUE/FALSE);
IS DISTINCT FROM ? works on both.

session_output.seq is allocated as MAX(seq)+1 inside the INSERT. That is atomic
on SQLite's single writer but not under Postgres READ COMMITTED with a
connection per request: a burst of five chunks came back numbered
[1,1,2,3,3], and a browser resuming from ?since= skips the repeat for good.
On Postgres the owning cli_sessions row is locked FOR UPDATE first; migration
025 makes (session_id, seq) unique as the backstop (the live data has no
duplicates).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scripts/pg-test-preload.mjs creates a throwaway database per test process and
points config.db.url at it through a loader hook, so the unchanged suite runs
on a real Postgres (PG_TEST_ADMIN_URL=... node --import ./scripts/pg-test-preload.mjs --test).

Fixtures that leaned on SQLite: ORDER BY rowid (created_at is already
distinct), a device-code seed that relied on INSERT OR REPLACE cascading
(an upsert on Postgres), the PRAGMA foreign_keys probe and the raw SQLite
migration replay are skipped there. sql-portability gains two guards: the two
migration directories must mirror each other, and no `IS NOT ?`.

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

socket-security Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

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.27710010091100

View full report

Comment thread apps/pwa/README.md Fixed
Comment thread apps/pwa/scripts/pg-test-preload.mjs Fixed
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

ThreatCrush Security Scan

40 finding(s) in the 46 file(s) this pull request changes.

MEDIUM: 40

Severity Rule Location
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:139
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:153
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:179
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:373
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:377
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:422
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:674
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:870
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:872
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:931
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:977
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1047
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1150
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1173
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1195
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1394
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1544
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1693
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1699
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1746
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1795
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1826
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:1926
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2045
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2057
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2069
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2103
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2301
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2314
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2340
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2346
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2350
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2360
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2491
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2643
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2651
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2659
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2689
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:2828
MEDIUM sql-template-interpolation apps/pwa/test/moshpit-terms.test.mjs:207
64 pre-existing finding(s) elsewhere in the repository — **HIGH/CRITICAL**: 8 | **MEDIUM**: 45 | **LOW**: 11

Not introduced by this pull request. The full set is in the Security tab.

Severity Rule Location
HIGH js-ssrf-outbound-request apps/pwa/public/sw.js:45
HIGH tls-verification-disabled apps/pwa/src/lib/moshpit-gateway.mjs:299
HIGH sh-remote-script-execution install.sh:86
HIGH sh-remote-script-execution install.sh:90
HIGH sh-remote-script-execution install.sh:258
HIGH sh-remote-script-execution install.sh:269
HIGH sh-remote-script-execution install.sh:275
HIGH tls-verification-disabled src/dns.mjs:766
MEDIUM sql-template-interpolation apps/pwa/src/lib/moshpit-certs.mjs:44
MEDIUM sql-template-interpolation apps/pwa/src/lib/moshpit-certs.mjs:82
MEDIUM sql-template-interpolation apps/pwa/src/routes/mcp.mjs:87
MEDIUM js-unescaped-html-sink apps/pwa/src/routes/moshpit.mjs:2978
MEDIUM js-dynamic-code-execution apps/pwa/test/apikey-mask.test.mjs:129
MEDIUM sql-template-interpolation apps/pwa/test/credits-webhook-event-match.test.mjs:111
MEDIUM sql-template-interpolation apps/pwa/test/credits-webhook-event-match.test.mjs:131
MEDIUM sql-string-concatenation src/cli-schema.mjs:219
MEDIUM sql-string-concatenation src/cli-schema.mjs:306
MEDIUM sql-string-concatenation src/cli-schema.mjs:716
MEDIUM sql-string-concatenation src/cli-schema.mjs:749
MEDIUM sql-string-concatenation src/cli-schema.mjs:856

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

Snippets are redacted; ThreatCrush never prints matched credential material.

The sample admin URL carried a placeholder password, which the ThreatCrush
scanner reads as a database credential.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 96751e5 into main Sep 25, 2026
6 checks passed
@ralyodio
ralyodio deleted the port/pwa-libsql-pg branch September 25, 2026 16:41
@ralyodio ralyodio mentioned this pull request Oct 4, 2026
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