The workflow of Vite or Nodemon—but for SQL. Hot-reload SQL while you iterate. Build as migrations when it's time to ship.
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.
srtd intentionally separates iteration, review, and execution.
- Plain SQL files
- Concise and readable
- The primary thing you review in Git
- Generated from templates
- Explicit and deterministic
- Safe to deploy in CI and production
Templates evolve over time. Migrations are the execution record.
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.
npm install -g @t1mmen/srtd
cd your-supabase-project
# Create a template
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
# Start watch mode
srtd watchEdit hello.sql, save—it's live on your local database. No migration file, no restart, no waiting.
When ready to deploy:
srtd build # Creates supabase/migrations/20241226_srtd-hello.sql
supabase migration up # Deploy with Supabase CLIAlready have functions deployed? Create templates for them, then register so srtd knows they're not new:
srtd register existing_function.sql # Won't rebuild until the template actually changessrtd watch # Edit template → auto-applied to local DBHot reload is a DX feature. It exists to make iteration smooth and fast.
srtd build # → migrations/20250109123456_srtd-template-name.sql
supabase migration up # Deploy via Supabase CLIGenerated migrations fully redefine objects—databases prefer explicit redefinition, humans prefer small diffs. srtd optimizes for both in different layers.
Use .wip.sql files for experiments:
my_experiment.wip.sql → Applies locally, never builds to migration
- Applied during
watchandapply - Excluded from
build - Safe to experiment without affecting production
When ready: srtd promote my_experiment.wip.sql
Declare dependencies between templates with @depends-on comments:
-- @depends-on: helper_functions.sql
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.
Templates need to be idempotent—safe to run multiple times. This works great for:
| Object | Pattern |
|---|---|
| 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.
srtd # Interactive menu
srtd watch # Live reload—applies templates on save
srtd build # Generate migration files
srtd apply # Apply all templates once (no watch)
srtd register # Mark templates as already deployed
srtd promote # Convert .wip template to buildable
srtd clear # Reset build state
srtd init # Initialize config file
srtd doctor # Validate setup and diagnose issues
# Build options
srtd build --force # Rebuild all templates
srtd build --apply # Apply immediately after building
srtd build --bundle # Combine all into single migration
srtd build --no-deps # Disable dependency ordering
# Apply options
srtd apply --force # Force apply all templates
srtd apply --no-deps # Disable dependency ordering
# Clear options
srtd clear --local # Clear local build log only
srtd clear --shared # Clear shared build log only
srtd clear --reset # Reset everything to defaults
# Global options (all commands)
--json # Machine-readable output
--non-interactive # Disable prompts (for scripts)All commands support --json for machine-readable output (CI/CD, LLM integrations):
srtd build --json # Single JSON object with results array and summary
srtd watch --json # NDJSON stream (one event per line)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.
Defaults work for standard Supabase projects. Optional srtd.config.json:
Run srtd doctor to validate your configuration and diagnose setup issues.
The migrationFilename option lets you match your project's existing migration structure:
| Variable | Description | Example |
|---|---|---|
$timestamp |
Build timestamp (YYYYMMDDHHmmss) | 20240315143022 |
$migrationName |
Template name (without .sql) | create_users |
$prefix |
Migration prefix with trailing dash | srtd- |
Default (Supabase-style):
{ "migrationFilename": "$timestamp_$prefix$migrationName.sql" }
// → migrations/20240315143022_srtd-create_users.sqlDirectory per migration (Prisma-style):
{ "migrationFilename": "$timestamp_$migrationName/migration.sql" }
// → migrations/20240315143022_create_users/migration.sqlFlyway-style (V prefix):
{
"migrationFilename": "V$timestamp__$migrationName.sql",
"migrationPrefix": ""
}
// → migrations/V20240315143022__create_users.sqlNested directories are created automatically.
| File | Purpose | Git |
|---|---|---|
.buildlog.json |
What's been built to migrations | Commit |
.buildlog.local.json |
What's applied to your local DB | Gitignore |
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.
Bug fixes, docs, and test coverage welcome. See CONTRIBUTING.md.
For development: CLAUDE.md.
Built by Timm Stokke with Claude, after two years of being annoyed.



{ "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": "" }