Repository navigation
feat(api): move from Turso/libSQL to Postgres via @profullstack/libsql-pg - #130
Merged
Merged
Conversation
…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>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Contributor
ThreatCrush Security Scan52 finding(s) HIGH/CRITICAL: 3 | MEDIUM: 28 | LOW: 21
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
pgor readDATABASE_URL(thepgmention was mistaken), so the port uses the house shim.services/api/src/db.tsbuilds the client with@profullstack/libsql-pg@0.1.1(createClient({ url })). It keeps the@libsql/clientsurface over apgpool and rewrites SQLite idioms (datetime('now'),ON CONFLICT,?placeholders) per statement, so the queries indb.ts,store/db.tsandpush/routes.tsare unchanged apart from the two hand ports below.DATABASE_URL(or the aliasTRONBROWSER_DB_URL) must bepostgres:///postgresql://. Missing,libsql://,file:or a setTRONBROWSER_DB_PATHthrows a clear error;assertDatabaseUrl()runs at start inservices/api/src/index.ts. No fallback to a file database.packages/storage/migrations-pg/(7 files), generated withnpx libsql-pg convert-schemaand reviewed. The converter promotedcreated_at/updated_at/last_seen_at(text with adatetime('now')default) totimestamptz; I also madeexpires_at(sessions, email_tokens),paid_at,scanned_at,provisioned_at,last_used_atanddeleted_attimestamptz, because the app stamps them withdatetime('now')(nownow()) or compares them against it.push_messages.expires_atstays a unix-secondsbigint; 0/1 flags (enabled,email_verified) staybigint.scripts/db-migrate.mjsappliesmigrations-pg(one transaction per file, recorded inschema_migrations) when the URL is Postgres; alibsql:///file:URL still applies the SQLitemigrations/so the old database can be kept in step until the cutover is proven.@libsql/clientmoved from dependencies to devDependencies (that path and the in-memory tests only)..env.exampleandpackages/storage/README.mdupdated.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 nodatetime(). The expiry is now computed in JS (expiresAt(ttl)) and bound as an ISO string into thetimestamptzcolumn.listLiveExtensions:LIKEbecameILIKE. SQLite'sLIKEignores ASCII case and store search relied on that; Postgres'sLIKEis case-sensitive.rowid,?NNN,json_each,COLLATE NOCASE,last_insert_rowid(),LIMIT -1, FTS5,GLOBorIS NOT <value>in this service.Tests
pnpm build,pnpm typecheckandpnpm testpass with no database (services/api: 14 files, 96 tests).push.test.tsruns the whole push protocol suite against in-memory SQLite by default; withTEST_DATABASE_URL=postgres://...it appliesmigrations-pg/0007_push.sqland runs the same requests through the shim.db.test.ts(new) covers the URL guard and expiry helper.Cutover recipe (coordinator)
Beyond the recipe: the copier coerces the ISO-text timestamps into the
timestamptzcolumns usinginformation_schema; checksessions.expires_atandemail_tokens.expires_atlanded (select count(*) from sessions where expires_at > now()). All primary keys are text, so there are no sequences to reset. RunANALYZEafter the load.🤖 Generated with Claude Code