srtd — Live-Reloading, declarative, SQL migration templates for Supabase and Postgres

The workflow of Vite or Nodemon—but for SQL.
Hot-reload SQL while you iterate. Build as migrations when it's time to ship.

Why srtd exists
Database development usually has a slow, frustrating feedback loop:
- Edit SQL in your IDE
- Copy to SQL editor or create migration
- Run it
- Hit an error? Fix in IDE, copy again, run again
- Works? Back to your IDE, until the next change
- Repeat
Code reviews aren't much better. Changing a single line in a function often shows up as a full rewrite in Git, making it hard to see what actually changed.
srtd exists to fix both problems:
- Fast local iteration with live reload
- Clean, reviewable diffs for database logic
- Real migrations for production safety
After searching for two years while building Timely's Memory Engine on Supabase, the itch needed to be scratched.
The core idea
srtd intentionally separates iteration, review, and execution.
Templates are for humans
- Plain SQL files
- Concise and readable
- The primary thing you review in Git
Migrations are for databases
- Generated from templates
- Explicit and deterministic
- Safe to deploy in CI and production
Templates evolve over time. Migrations are the execution record.
The diff problem—solved
Without templates, changing one line in a function means your PR shows a complete rewrite—the old DROP + CREATE replaced by a new one. Reviewers have to read the whole thing, and manually compare it to the last migration to touch the function to spot changes. Talk about friction!
With srtd, your PR shows what you actually changed;
|
Without srtd
+ DROP FUNCTION IF EXISTS calculate_total;
+ CREATE FUNCTION calculate_total(order_id uuid)
+ RETURNS numeric AS $fn$
+ BEGIN
+ RETURN (SELECT SUM(price * quantity) FROM order_items);
+ END;
+ $fn$ LANGUAGE plpgsql;
|
With srtd
CREATE FUNCTION calculate_total(order_id uuid)
RETURNS numeric AS $fn$
BEGIN
- RETURN (SELECT SUM(price) FROM order_items);
+ RETURN (SELECT SUM(price * quantity) FROM order_items);
END;
$fn$ LANGUAGE plpgsql;
|
git blame works. Code reviews are useful. Your database logic is treated like real code.
How To Use It
Quick start
npm install -g @t1mmen/srtd
cd your-supabase-project
mkdir -p supabase/migrations-templates
cat > supabase/migrations-templates/hello.sql << 'EOF'
DROP FUNCTION IF EXISTS hello;
CREATE FUNCTION hello() RETURNS text AS $$
BEGIN RETURN 'Hello from srtd!'; END;
$$ LANGUAGE plpgsql;
EOF
srtd watch
Edit hello.sql, save—it's live on your local database. No migration file, no restart, no waiting.
When ready to deploy:
srtd build
supabase migration up
Already have functions deployed? Create templates for them, then register so srtd knows they're not new:
srtd register existing_function.sql
Development loop (watch mode)
srtd watch
Hot reload is a DX feature. It exists to make iteration smooth and fast.

Shipping to production (generate migrations)
srtd build
supabase migration up
Generated migrations fully redefine objects—databases prefer explicit redefinition, humans prefer small diffs. srtd optimizes for both in different layers.
WIP templates
Use .wip.sql files for experiments:
my_experiment.wip.sql → Applies locally, never builds to migration
- Applied during
watch and apply
- Excluded from
build
- Safe to experiment without affecting production
When ready: srtd promote my_experiment.wip.sql

Template dependencies
Declare dependencies between templates with @depends-on comments:
CREATE FUNCTION complex_calc() ...
During apply and build, templates are sorted so dependencies run first. Circular dependencies are detected and reported. Use --no-deps to disable.
What works as templates
Templates need to be idempotent—safe to run multiple times. This works great for:
| Functions | DROP FUNCTION IF EXISTS + CREATE FUNCTION |
| Views | CREATE OR REPLACE VIEW |
| RLS Policies | DROP POLICY IF EXISTS + CREATE POLICY |
| Triggers | Drop + recreate trigger and function |
| Roles | REVOKE ALL + GRANT |
| Enums | ADD VALUE IF NOT EXISTS |
Not for templates: Table structures, indexes, data modifications—use regular migrations for those.
Commands
srtd
srtd watch
srtd build
srtd apply
srtd register
srtd promote
srtd clear
srtd init
srtd doctor
srtd build --force
srtd build --apply
srtd build --bundle
srtd build --no-deps
srtd apply --force
srtd apply --no-deps
srtd clear --local
srtd clear --shared
srtd clear --reset
--json
--non-interactive
JSON output
All commands support --json for machine-readable output (CI/CD, LLM integrations):
srtd build --json
srtd watch --json
Output includes success, command, timestamp, and command-specific fields. Errors use a top-level error field.
Works great with Claude Code background tasks—pair with the srtd-cli skill and srtd rule for SQL template guidance.
Configuration
Defaults work for standard Supabase projects. Optional srtd.config.json:
{
"templateDir": "supabase/migrations-templates",
"migrationDir": "supabase/migrations",
"pgConnection": "postgresql://postgres:postgres@localhost:54322/postgres",
"migrationPrefix": "srtd",
"migrationFilename": "$timestamp_$prefix$migrationName.sql",
"wipIndicator": ".wip",
"wrapInTransaction": true,
"filter": "**/*.sql",
"banner": "/* Auto-generated by srtd. Edit the template, not this file. */",
"footer": ""
}
Run srtd doctor to validate your configuration and diagnose setup issues.
Custom migration paths
The migrationFilename option lets you match your project's existing migration structure:
$timestamp | Build timestamp (YYYYMMDDHHmmss) | 20240315143022 |
$migrationName | Template name (without .sql) | create_users |
$prefix | Migration prefix with trailing dash | srtd- |
Examples
Default (Supabase-style):
{ "migrationFilename": "$timestamp_$prefix$migrationName.sql" }
// → migrations/20240315143022_srtd-create_users.sql
Directory per migration (Prisma-style):
{ "migrationFilename": "$timestamp_$migrationName/migration.sql" }
// → migrations/20240315143022_create_users/migration.sql
Flyway-style (V prefix):
{
"migrationFilename": "V$timestamp__$migrationName.sql",
"migrationPrefix": ""
}
// → migrations/V20240315143022__create_users.sql
Nested directories are created automatically.
State tracking
.buildlog.json | What's been built to migrations | Commit |
.buildlog.local.json | What's applied to your local DB | Gitignore |
FAQ
Why do generated migrations redefine entire objects?
Full redefinitions ensure deterministic, idempotent execution across environments. Templates encode intent; migrations encode execution.
Can I use hot reload in production?
No. Hot reload is strictly a local development feature. Production deploys always use generated migrations.
Why plain SQL instead of a DSL?
SQL is already the right language. srtd adds workflow improvements, not syntax.
What does "srtd" stand for?
Supabase Repeatable Template Definitions—but then general Postgres support happened, and... naming things is hard.
Contributing
Bug fixes, docs, and test coverage welcome. See CONTRIBUTING.md.
For development: CLAUDE.md.
More
Built by Timm Stokke with Claude, after two years of being annoyed.