feat(db): move from SQLite Cloud to Turso - #50
Merged
Merged
Conversation
Today's outage was a parked SQLite Cloud free node: every query and every
new connection answered error 10010, and only a human restart from the
dashboard could clear it. Turso sleeps too — a free group is archived
after ten days idle — but it comes back through an API call, so the same
failure becomes something a cron can heal instead of something that waits
on someone noticing.
The app talks to the database through exactly one surface, `db.sql`, so
the swap lives in lib/db.ts. libSQL rows are array-like and object-like
and serialize to plain named objects, which means all 77 call sites keep
working untouched: `rows.length`, destructuring a single COUNT row,
`NextResponse.json(rows)`, and the bounty claim's `RETURNING id` guard
all behave as before. Verified by running the app against both a local
file database and the real Turso database.
One genuine incompatibility: libSQL refuses `undefined`, which the routes
pass freely for absent optional fields. Coercing to NULL at the binding
boundary keeps a missing image_url a NULL instead of a 500.
The reconnect-and-retry dance is gone with the old driver. There is no
long-lived websocket to go stale, so the failure that silently emptied
/blog cannot recur in that form.
Two pre-existing problems surfaced while recreating the schema, both
fixed here:
* lib/schema.sql stopped at blog_posts, omitting coupon_votes and
bounties, so anyone rebuilding from it got a silently incomplete
database. It is now dumped from a real migrated database.
* idx_coupons_store, idx_stores_slug and idx_categories_slug were
declared in schema.sql but never created by migrate.mjs, so no
database built by the script has ever had them.
Footer, privacy policy and llms.txt named SQLite Cloud as the database
provider; the privacy page lists it as a third-party service, so leaving
it would have been inaccurate about where user data lives.
Not included: the data itself. The export has to come off the SQLite
Cloud node, which is still parked, so the Turso database currently holds
the schema and nothing else. Railway and vault variables are deliberately
untouched — cutting over before the import would point production at an
empty database.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan17 finding(s) MEDIUM: 17
Snippets are redacted; ThreatCrush never prints matched credential material. |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
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.
Replaces SQLite Cloud with Turso (libSQL), so an idle database is something a cron can revive instead of something that waits on a human at a dashboard.
Why
Today's outage — coupon submission failing with "The coupon database is temporarily unavailable" — was a parked SQLite Cloud free node. Error 10010 on every query and every new connection, revivable only from the dashboard, with no account-level API key in any vault.
Turso also sleeps, and I want to be precise about that rather than oversell it:
POST …/groups/{group}/unarchivefile:local.db, same clientThe win is recoverability, not "never sleeps".
Why the diff is small
All 77
db.sqlcalls across 22 files go through one tagged-template surface, and libSQL rows are array-like and object-like, serializing to plain named objects. So no call site changed.lib/db.tsis the migration.One real incompatibility: libSQL refuses
undefined(undefined cannot be passed as argument to the database), which the routes pass freely for absent optional fields. Coerced toNULLat the binding boundary.The reconnect/retry logic is gone with the old driver — no long-lived websocket to go stale, so the bug that silently emptied
/blogcan't recur in that form.Two pre-existing bugs found while recreating the schema
lib/schema.sqlwas incomplete — it stopped atblog_posts, omittingcoupon_votesandbounties. Rebuilding from it gave you a silently broken database. Now dumped from a real migrated database.idx_coupons_store,idx_stores_slug,idx_categories_slugexisted inschema.sqlbut not inmigrate.mjs, so no database built by the script has ever had them — production included. Added.Also updated the footer, privacy policy and
llms.txt, which all named SQLite Cloud as the database provider. The privacy page lists it as a third-party service, so leaving it would have been factually wrong about where user data lives.Verification
Ran the app against a local file DB and the real remote Turso database:
/,/blog,/search,/stores,/stores/[slug],/bounties,/coupons/[id],/sitemap.xml,/blog/rss.xml, and the/api/*handlers201 Createdon both, with fields omitted from the payload landing as properNULLs (not"undefined") — this is the exact operation that failed in production{"success":true,"votes":1}; duplicate vote →409 already_voted409(theRETURNING idrace guard, the riskiest SQL shape here)pnpm typecheckpasses;pnpm lintreports the same 12 pre-existing errors as before, none in changed filesWhat this PR deliberately does NOT do
No data is migrated, and no environment variables are changed. The export has to come off the SQLite Cloud node, which is still parked — so the Turso database currently holds the schema and nothing else, and Railway still points at SQLite Cloud.
Merging this alone does not cut over. Remaining steps, in order:
libsql://c0upons-profullstack.aws-us-west-2.turso.ioTURSO_DATABASE_URL+TURSO_AUTH_TOKENin Railway and thec0upons--prodvault (turso db tokens create c0upons)SQLITECLOUD_URLin place for one deploy so rollback is a variable change🤖 Generated with Claude Code