21. Schema Migrations
Alex wants schema changes that are versioned, reviewable, and repeatable across environments.
Migrations are versioned SQL files in your project's migrations/ directory, applied in order against your project's database schema. The platform records what has been applied, so migrations up only runs what's pending — re-running it is always safe, locally or in CI. Use migrations for anything the table editor doesn't cover: composite unique constraints, CHECK constraints, partial indexes, triggers, backfills.
The workflow
A realistic example
migrations/0001_create_listings.sql — a table with a composite natural key, a CHECK constraint, a partial index, and an RLS policy:
CREATE TABLE listings (
id uuid PRIMARY KEY DEFAULT public.uuid_generate_v4(),
user_id uuid NOT NULL,
source text NOT NULL,
source_id text NOT NULL,
status text NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'expired', 'flagged')),
price numeric(8,2),
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (source, source_id) -- composite natural key: no dupes per source
);
CREATE INDEX idx_listings_active
ON listings (user_id, created_at)
WHERE status = 'active'; -- partial index: hot rows only
ALTER TABLE listings ENABLE ROW LEVEL SECURITY;
CREATE POLICY listings_owner ON listings
USING (public.is_service_role() OR user_id = public.current_end_user_id())
WITH CHECK (public.is_service_role() OR user_id = public.current_end_user_id());How it executes
- Each migration runs in one transaction — if any statement fails, nothing from that file is applied.
- It runs inside your project schema: use unqualified table names. Multi-statement files are expected.
- Applied versions are recorded with a checksum. Re-applying an identical file is a no-op; editing an already-applied file is rejected — add a new version instead.
- There are no down migrations — to undo something, write a new forward migration.
- Concurrent runs are serialized per project, so two CI jobs can't race each other.
Running migrations in CI
Authenticate with a Personal Access Token (Account → Tokens) stored as a CI secret. Example GitHub Actions step:
- name: Apply database migrations
run: |
brew install stgime/tap/eurobase # or download the binary
eurobase login --token "$EUROBASE_PAT"
eurobase switch my-project
eurobase migrations up
env:
EUROBASE_PAT: ${{ secrets.EUROBASE_PAT }}What migration SQL can't do (and why)
Migrations run with your project's developer credentials (PAT or console session) — never with API keys: a leaked server key must not be able to alter your schema. Each migration executes under a database role scoped to only your project's schema, so it physically cannot read or write another project or the platform's own tables — that boundary is enforced by Postgres, not just by validation. On top of that, the platform rejects operations that try to reach outside your project before they run:
- References to other schemas (
public.*,pg_catalog, other tenants). Exception: the RLS helperspublic.is_service_role(),public.current_end_user_id(), andpublic.uuid_generate_v4(). GRANT/REVOKE,SET ROLE/search_path— access control belongs to the platform.- Transaction control (
COMMIT,BEGIN) — your file already runs in a transaction. SECURITY DEFINERfunctions,CREATE EXTENSION,COPY.
Plain LANGUAGE plpgsql functions and triggers are fine. Bulk data loads belong in the SDK/REST data path, not migrations.