Skip to content

Migrate from Supabase to Turso (libSQL/SQLite) - #234

Merged
ralyodio merged 3 commits into
masterfrom
worktree-turso-migration
Aug 11, 2026
Merged

Migrate from Supabase to Turso (libSQL/SQLite)#234
ralyodio merged 3 commits into
masterfrom
worktree-turso-migration

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Replaces every Supabase dependency with Turso: Postgres for data, RLS for tenant isolation, pgmq for the job queue, Supabase Auth for sessions, and Supabase Realtime for live logs.

See docs/Turso-Migration.md for the full write-up.

What changed

ConcernBeforeAfter
Driverpg.Pool@libsql/client
Migrationssupabase db pushnode scripts/db-migrate.js
Tenant isolationRow Level Securitypackages/shared/lib/authz.js
AuthSupabase Auth (JWT)Self-hosted sessions (scrypt)
Job queuepgmq extensionSQLite queue with visibility timeouts
Live logsSupabase RealtimeServer-Sent Events
Secret encryption(claimed, never implemented)AES-256-GCM

Two things worth reviewer attention

RLS is gone. Under Supabase a query that forgot its where owner = ... still returned only the caller's rows. That backstop no longer exists — an unfiltered query now returns every tenant's data. All access goes through authz.js; cross-tenant isolation is covered by tests.

Write transactions are serialised in-process.@libsql/client multiplexes over a single connection, so two overlapping write transactions interleave: the loser gets SQLITE_BUSY on BEGIN and the winner then fails its COMMIT with "SQL statements in progress". Retrying alone livelocks. SQLite allows one writer anyway, so this costs no real concurrency.

Pre-existing bugs fixed

  • Secrets were stored in plaintext — the code claimed a database trigger encrypted them; no such trigger existed in any migration. POST /api/secrets also wrote columns the table doesn't have, so it could never have succeeded.
  • /api/workflows/[id]/versions queried a workflow_versions table no migration created.
  • Publishing a workflow wasn't atomic — a failure mid-way left it both published and un-archived.
  • The webhook route imported a non-existent export and enqueued onto an in-process EventEmitter no deployed orchestrator could observe.
  • Webhook signature comparison wasn't constant-time.
  • The root .env was never read (db.js resolved the repo root one level short).
  • src/nodes/loop.test.js never ran — missing node:test import.

Railway build fix

The build was failing on npm install. Railpack detected npm because pnpm-lock.yaml was gitignored and packageManager was unset, and npm can't resolve pnpm's workspace:* protocol. Both fixed; @libsql/client is marked external since its native binding can't be bundled.

Testing

368 passing — 110 vitest (against real libSQL, not mocks) plus 258 node:test. Verified end-to-end against the built server: signup, session cookies, cross-tenant isolation, publish/versioning, webhook ingest, and secrets encrypted at rest.

Not included

No automated data migration from an existing Supabase instance — the type and auth-model changes mean rows can't be copied verbatim, and Supabase password hashes can't be exported in a form scrypt can verify. Email verification on signup is also dropped (no mail provider); tracked as #47.

Draft because migrating live data and provisioning the production Turso database still need a decision.

🤖 Generated with Claude Code

Replaces every Supabase dependency: Postgres for data, RLS for tenant
isolation, pgmq for the job queue, Supabase Auth for sessions, and
Supabase Realtime for live logs.
Data layer
- packages/shared/lib/db.js now uses @libsql/client. Keeps the same
one/oneOrNone/manyOrNone/none/tx API and translates $1 placeholders to ?.
- Serialises write transactions in-process. @libsql/client multiplexes over a
single connection, so overlapping transactions make the winner fail its
COMMIT with "SQL statements in progress" — retrying alone livelocks.
- workers/lib/db.js was a byte-for-byte copy; it now re-exports the shared one.
Schema
- 1252 lines of Postgres migrations ported to migrations/*.sql:
uuid->text, jsonb->text, timestamptz->text, bytea->blob, bigserial->integer.
- Event partitioning dropped (no SQLite equivalent); the (run_id, ts) index
carries the replay path instead.
- scripts/db-migrate.js applies SQL directly with a checksummed ledger,
replacing the shell-out to the Supabase CLI.
Authorization
- RLS is gone, so an unfiltered query now returns every tenant's rows.
packages/shared/lib/authz.js applies the same scoping the policies did.
Auth
- Self-hosted sessions: scrypt hashing, opaque tokens stored only as SHA-256,
so a database leak cannot be replayed. Logout revokes immediately.
- Login/signup moved to server form actions.
Queue
- pgmq reimplemented on SQLite, preserving visibility-timeout semantics.
- Browsing the DLQ no longer increments read_ct.
Live logs
- Supabase Realtime replaced by SSE at /api/runs/[id]/events, resuming from
Last-Event-ID.
Pre-existing bugs fixed along the way
- Secrets were stored in plaintext; a claimed encryption trigger never existed.
Now AES-256-GCM. POST /api/secrets also wrote columns the table lacks.
- /api/workflows/[id]/versions queried a table no migration created.
- Publishing a workflow was not atomic.
- The webhook route imported a non-existent export and enqueued onto an
in-process EventEmitter no deployed orchestrator could see.
- Webhook signature comparison was not constant-time.
- The root .env was never read; db.js resolved the repo root one level short.
- src/nodes/loop.test.js never ran (missing node:test import).
Build
- Railpack fell back to npm because pnpm-lock.yaml was gitignored and
packageManager was unset; npm cannot resolve workspace:*. Both fixed.
- @libsql/client marked external — its native binding cannot be bundled.
Tests: 368 passing (110 vitest against real libSQL, 258 node:test).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actionsBot commented Aug 11, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

17 finding(s)

HIGH/CRITICAL: 6 | MEDIUM: 10 | LOW: 1

SeverityRuleLocation
HIGHsecret-database-urlapps/web/.env.example:9
HIGHsecret-database-urldocs/Environment-Setup.md:46
HIGHsecret-database-urldocs/Environment-Setup.md:132
HIGHsecret-slack-webhookdocs/WEBHOOK_CONFIGURATION.md:145
HIGHsecret-database-urlRAILWAY_DEPLOYMENT.md:58
HIGHsecret-generic-credentialsrc/nodes/README.md:271
MEDIUMsql-template-interpolationapps/web/src/routes/api/secrets/[id]/+server.js:80
MEDIUMsql-template-interpolationapps/web/src/routes/api/workflows/[id]/+server.js:141
MEDIUMsql-template-interpolationdocs/Turso-Migration.md:70
MEDIUMmanifest-install-lifecycle-scriptpackage.json:9
MEDIUMjs-shell-exec-interpolationscripts/generate-issue-prds.mjs:71
MEDIUMjs-shell-exec-interpolationscripts/generate-issue-prds.mjs:586
MEDIUMjs-shell-exec-interpolationscripts/generate-issue-prds.mjs:651
MEDIUMjs-shell-exec-interpolationscripts/generate-issue-prds.mjs:685
MEDIUMjs-shell-exec-interpolationscripts/gh-project-status.js:71
MEDIUMjs-shell-exec-interpolationscripts/github-issues-sync.mjs:33
LOWsecret-generic-credentialsrc/nodes/webhook.test.js:287

Snippets are redacted; ThreatCrush never prints matched credential material.

Comment threadpackages/shared/lib/auth.test.js Fixed
Comment threadpackages/shared/lib/auth.test.js Fixed
Comment threadpackages/shared/lib/auth.test.js Fixed
Comment threadpackages/shared/lib/auth.test.js Fixed
Comment threadpackages/shared/lib/auth.test.js Fixed
Comment threadpackages/shared/lib/auth.test.js Fixed
Comment threadpackages/shared/lib/auth.test.js Fixed
try {
const { error } = await supabase.from('secrets').delete().eq('id', id);
const { rowsAffected } = await db.none(
`delete from secrets where id = ? and project_id in (${ownedProjectIdsSql()})`,

return json({ workflow: data });
const workflow = await db.oneOrNone(
`update workflow_definitions set ${updates.join(', ')}

```js
const rows = await db.manyOrNone(
`select * from secrets where project_id in (${ownedProjectIdsSql()})`,
@socket-security

socket-securityBot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

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

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​libsql/​client@​0.15.1510010010089100

View full report

@socket-security

socket-securityBot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

ActionSeverityAlert (click "▶" to expand/collapse)
WarnMedium
Deprecated by its maintainer: npm node-domexception

Reason: Use your platform's native DOMException instead

From:pnpm-lock.yamlnpm/@libsql/client@0.15.15npm/node-domexception@1.0.0

ℹ Read more on: This package | This alert | What is a deprecated package?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Research the state of the package and determine if there are non-deprecated versions that can be used, or if it should be replaced with a new, supported solution.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/node-domexception@1.0.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Two failures, both of which break the build regardless of package manager.
1. postinstall ran `npx @socketsecurity/socket-patch` inline. The patcher
exits 1 when SOCKET_API_TOKEN is unset, so any install without a token
failed outright and took the build with it. This is what Railway hit:
npm error command sh -c npx @socketsecurity/socket-patch apply ...
npm error No SOCKET_API_TOKEN set.
Patching is hardening, not a build requirement, so it moves to
scripts/socket-patch.mjs, which skips when no token is configured and
never exits non-zero whatever the patcher does.
2. pnpm 10+ refuses to run a dependency's build scripts unless approved, and
exits non-zero when any are ignored — so `pnpm install --frozen-lockfile`
returned 1 on ERR_PNPM_IGNORED_BUILDS for esbuild, which Vite needs to
bundle the web app. Approved via allowBuilds in pnpm-workspace.yaml
(pnpm 11 reads it there, not from package.json).
Also drops the redundant "dependencies" script, which duplicated postinstall.
Verified from a wiped node_modules: install exits 0, the web app builds, and
the orchestrator boots. Tests: 368 passing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment threadpackage.json
"scripts": {
"mh": "node packages/cli/bin/mh.js",
"postinstall": "npx @socketsecurity/socket-patch apply --silent --ecosystems npm && echo \"✅ MeshHook workspace installed\"",
"postinstall": "node scripts/socket-patch.mjs",
@ralyodio
ralyodio marked this pull request as ready for review August 11, 2026 13:14
ThreatCrush failed PR #234 with 7 high-severity "Hardcoded Credential"
findings, all of them fixture passwords in auth.test.js ("password123").
They were never real credentials, but seven findings on one file bury
anything genuine in noise, and the check blocks the merge.
The fixtures are now generated per run, which also stops any test depending
on a specific secret value.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 5b2f56b into masterAug 11, 2026
3 checks passed
@ralyodio
ralyodio deleted the worktree-turso-migration branch August 11, 2026 13:16
Sign up for freeto 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

@ralyodio@github-advanced-security