GUIDE

How to Use the diff Command Modes

One command for every step: report, review, rehearse and apply.

The SupaForge CLI is open source and on npm: npm install -g @akal/supaforge. A hosted dashboard is on the way - to hear when it opens.

Overview

supaforge diff compares a source environment with a target and reports everything that differs. Flags decide how far it goes:

  • No flags - a summary: which checks drifted, and how badly.
  • --detail - every finding, with the SQL that fixes it.
  • --apply - runs the fixes against the target.
  • --apply --dry-run - prints the fixes in the order they would run, and runs nothing.
  • --apply --prove - rehearses the fixes on a throwaway copy of the target first.

Nothing is written to the target without --apply.

Mode 1 - Summary (Default)

Summary mode
supaforge diff

Real output for a staging project one migration ahead of production:

Output
SupaForge scan complete: 4 drift issues found across 3 checks.
5 checks were skipped - coverage is partial.
Source: staging → Target: production

  ● Layer 1 (Schema):                   2 issues [WARNING]  (+1 reported under RLS Policies)
  ● Layer 2 (RLS Policies):             1 issue [CRITICAL]
  ✓ Layer 3 (RLS Coverage):             0 issues
  ○ Layer 4 (Edge Functions):           skipped - no projectRef or accessToken configured
  ✓ Layer 5 (Storage):                  0 issues
  ○ Layer 6 (Auth Config):              skipped - no apiUrl/projectRef or accessToken configured
  ● Layer 7 (Cron Jobs):                1 issue [WARNING]
  ○ Layer 8 (Reference Data):           skipped - no tables configured in checks.data.tables
  ✓ Layer 9 (Webhooks):                 0 issues
  ...
  ✓ Layer 14 (Postgres Roles & Grants): 0 issues
  ○ Layer 15 (Queues):                  skipped - pgmq extension not installed
  ○ Layer 16 (Partition Sets):          skipped - pg_partman extension not installed

Drift score: 70/100 (11 of 16 checks compared)
Posture score: 100/100 (target only - RLS coverage, migration history)

  → Run with --detail to see SQL diffs
  → Run with --apply to fix drift

A check that can't run - here, the ones that need a Supabase access token - says so and why. It is never shown as a clean pass. The drift score is 100 when the two environments match.

The command exits 1 when it finds critical drift, so it works as a gate as it is. For pipelines, --ci adds GitHub annotations, JSON on stdout and distinct exit codes: 0 clean, 1 drift, 2 error.

Mode 2 - Detail (--detail)

Detail mode
supaforge diff --detail
Output (excerpt)
── Layer 1: Schema ────────────────────────────────────────

  ⚠ [WARNING] Index missing: public.orders_user_id_idx
    SQL fix (UP):
      CREATE INDEX orders_user_id_idx ON public.orders USING btree (user_id);

── Layer 2: RLS Policies ──────────────────────────────────

  ✖ [CRITICAL] Missing RLS policy: Users can read own profile
    Policy "Users can read own profile" on public.profiles exists in source
    but is missing from target. This is a CVE-2025-48757 risk pattern.

    SQL fix (UP):
      CREATE POLICY "Users can read own profile"
        ON "public"."profiles"
        AS PERMISSIVE
        FOR SELECT
        TO "authenticated"
        USING ((auth.uid() = id))
      ;

Combine --detail with --check to focus on one check:

Detail for RLS only
supaforge diff --check=rls --detail

Mode 3 - Apply (--apply)

Before applying, see the exact plan in the order it will run:

The plan, without running it
supaforge diff --apply --dry-run
Output
Would apply 4 fix(es), in this order:
  1. [schema] schema-alter-1
     ALTER TABLE "profiles" ADD COLUMN "avatar_url" text;
  2. [schema] schema-create-index-2
     CREATE INDEX orders_user_id_idx ON public.orders USING btree (user_id);
  3. [cron] cron-missing-nightly-cleanup
     SELECT cron.schedule('nightly-cleanup', '0 3 * * *', $$DELETE FROM public.orders WHERE total = 0$$);
  4. [rls] rls-missing-public.profiles.Users can read own profile
     CREATE POLICY "Users can read own profile" ON "public"."profiles" ...

  Nothing was executed. Drop --dry-run to apply.

Then apply. With --prove, SupaForge first replays the fixes on a throwaway copy of the target and applies nothing unless the result matches the source - or if the rehearsal can't run at all:

Rehearse, then apply
supaforge diff --apply --prove
Output
  Proving convergence on a throwaway clone…
  Not proved - act outside the proved schemas or on the whole server: cron ×1
  Converged - the migration reproduces the source exactly.

Applied 4 fix(es):
  ✓ [schema] schema-alter-1
  ✓ [schema] schema-create-index-2
  ✓ [cron] cron-missing-nightly-cleanup
  ✓ [rls] rls-missing-public.profiles.Users can read own profile

The fixes run in dependency order and in one transaction: if one fails, none of them stay. Anything that would lose data - dropping a table or column, deleting rows, removing a policy - is reported but held back until you add --allow-destructive. Running the same apply again finds nothing left to do.

The rehearsal copies a sample of the target's rows as well. A fix that would lose them, or change their values, fails the proof, and tables the fixes would rewrite are named. Each finding also says what its fix does on a live database: whether it blocks reads or writes, rewrites the table, or scans every row.

To apply one check only:

Apply RLS drift only
supaforge diff --check=rls --apply

Or pick individual findings with --only, using the ids from --json (globs allowed):

Apply chosen findings
supaforge diff --apply --only='schema-create-*'

One Instant per Database

A scan reads each database as of a single moment, so a migration or change made while it runs is seen by every check or by none, and the report never describes a database that did not exist. As with pg_dump, what the scan reads stays locked until it ends; --no-snapshot reads each query on its own instead.

Schema Files as the Source

If your schema lives in SQL files, such as supabase/schemas, compare the directory with an environment instead of a second database. SupaForge loads the files into a throwaway database on the target's server, compares them and drops it again. The files can be in any order:

Schema files against production
supaforge diff --source-dir supabase/schemas --target=production

Every other flag works the same, including --apply and --prove.

supaforge sync - Shorthand for --apply

supaforge sync is supaforge diff --apply. It takes the same flags, including --check, --skip, --source, --target, --dry-run and --prove:

Sync with source/target override
supaforge sync --source=staging --target=production

supaforge hukam - An Alias for diff

supaforge hukam works exactly like supaforge diff, with the same flags and output:

hukam (same as diff)
supaforge hukam --detail

Hukam (ਹੁਕਮ) is a Punjabi word meaning "command" or "order" - a nod to the Sikh tradition of taking a daily hukamnama. It's hidden from --help.

Choosing the Right Mode

  • A quick look, or a CI gate - supaforge diff, or --ci in a pipeline.
  • Before changing anything - --detail to read the SQL, --apply --dry-run to see the order.
  • Applying to production - --apply --prove.
  • A focused change - --check, --skip, --tables or --only.

After each run a dim tip: line suggests the most useful next step for what was found.

Install

Install SupaForge
npm install -g @akal/supaforge

Summary

One command covers the whole loop: supaforge diff to see what drifted, --detail to read the fixes, --dry-run to see their order, and --apply --prove to make the target match the source safely.

Ready to try SupaForge?

Install the CLI and run your first diff in a minute. Nothing is changed until you add --apply.

npm install -g @akal/supaforge
View on GitHub →