Safe Schema Migration
The schema (created with execute_sql) and your handler code must agree on column
names and nullability. A mismatch is a top cause of HTTP 500s — e.g. code that inserts
an image_key into a table whose only column is image_url TEXT NOT NULL: every
insert violates the NOT NULL constraint and the request 500s.
Golden rules
- Change schema BEFORE deploying code that depends on it. Run
execute_sqlfirst, thendeploy. Otherwise the new code runs against the old schema and 500s. - Adding a column to a table that already has rows? Make it nullable or give it a
DEFAULT. A bare
ADD COLUMN ... NOT NULL(no default) fails on a non-empty table. - Name columns for what they hold. If you store the key from
uploadFile, call itimage_key, notimage_url. See onvibe://docs/uploads. - Keep statements idempotent so re-running is safe.
Patterns
Add a column (safe on populated tables)
ALTER TABLE photos ADD COLUMN IF NOT EXISTS image_key TEXT; -- nullable: always safe
ALTER TABLE photos ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'pending'; -- NOT NULL needs a DEFAULT
Rename a column without downtime
Do it in two deploys so old and new code both work in between:
-- 1. add the new column, backfill, keep the old one
ALTER TABLE photos ADD COLUMN IF NOT EXISTS image_key TEXT;
UPDATE photos SET image_key = image_url WHERE image_key IS NULL;
-- 2. deploy code that reads/writes image_key
-- 3. once nothing uses it, drop the old column:
ALTER TABLE photos DROP COLUMN IF EXISTS image_url;
Backfill, then tighten
Only add a NOT NULL constraint after every row has a value:
ALTER TABLE photos ADD COLUMN IF NOT EXISTS image_key TEXT;
UPDATE photos SET image_key = '...' WHERE image_key IS NULL;
ALTER TABLE photos ALTER COLUMN image_key SET NOT NULL;
Inspect the live schema when unsure
Use query_sql (read-only) to see what actually exists before changing code:
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'photos'
ORDER BY ordinal_position;
If a deploy starts returning 500s right after a schema or code change, this mismatch is the first thing to check — confirm column names and nullability match your handler, then run get_app_logs for the exact constraint error.
Versioned migrations (recommended over ad-hoc execute_sql)
Instead of running schema changes by hand with execute_sql, store them as files and let
the platform apply them once, in order, and track what's been applied.
- Write each change as a file at
migrations/<version>.sql. Use a sortable, timestamp-based version so lexicographic order = chronological order, e.g.migrations/20260101120000_add_image_key.sql. - Keep migrations forward-only and additive (CREATE TABLE, ADD COLUMN ... with a DEFAULT or nullable). Don't edit an already-applied migration — add a new one.
- Call
apply_migrations({ project_id }). It applies only the pending ones, each inside a transaction, and records them in aschema_migrationstable inside your database. Safe to re-run: applied migrations are skipped. When there are pending migrations, it first saves a restore point (code + database snapshot), returned asrestore_point— pass that label torollback_to_versionif a migration corrupts data. The restore point is auto-removed after a few days, so revert promptly if needed.
-- migrations/20260101120000_add_image_key.sql
ALTER TABLE photos ADD COLUMN IF NOT EXISTS image_key TEXT;
Iterating on a production app safely (drafts)
When an app already has real user data, don't experiment on it directly. Work in a draft:
create_draft({ source_project_id })→ a private copy with its own URL and a snapshot of the app's code + database. Drafts don't count against your project limit, and you can draft even a locked app.- Iterate in the draft:
edit_file/deployfor code, addmigrations/<version>.sqlfiles andapply_migrationsfor schema,upload_assetfor assets. Test on the draft URL. apply_changes({ draft_project_id })→ pushes the draft's code, pending migrations and app assets to the main app and deletes the draft. The main app's database rows and user uploads are preserved — only the schema is evolved via your migrations. A restore-point version (code + database) is created first, sorollback_to_versioncan undo it. If the main app changed since the draft was created, apply_changes aborts (passforce: trueto override). To discard a draft instead,delete_projectit.