Docs Supabase migration report

DocsReference

#The Supabase migration report (JSON), version 2

fz import supabase inspect --json writes this document; the later importer steps (#659, #660, #661) and the dashboard read it. The Rust types are cratefield_import_supabase::Report and its fields (crates/import-supabase/src/report.rs), which are the definition; this page is the reader's guide. supabase.md explains the command.

#Stability rules

  • report_version is 2. It changes when a field is removed or renamed, or changes meaning — 1 → 2 (issue #723) because a section the role cannot see is null where it used to be 0, and storage.counts_exact is gone. Adding a field does not change it: a reader ignores fields it does not know.
  • Every list is sorted — by schema, then name, then whatever makes the key unique; findings by id — so two inspections of an unchanged project produce the same bytes. There is no timestamp: when it ran is the run directory's business, not the report's.
  • Enumerations are lower-case snake_case strings.
  • Nothing secret is in it: no credential, no password hash, no email, no row data. The connection is named by host, port and database alone, and every free-text SQL fragment (policy expressions, column defaults, check constraints, cron commands) has passed through the harness's log scrubber.
  • An optional fact is null when it was not inspected or the role cannot see it — never a 0 or an empty list, which would claim a fact the report does not have.

#Top level

FieldTypeMeaning
report_versioninteger2
toolobjectname, version of the crate that wrote it
projectobjectref, host, port, database, server_version
read_onlyobjectwhat kept the inspection from writing, as observed (below)
coverageobjectdatabase, management_api, policy_classifier (each inspected, not_inspected, not_visible or failed) and sections (below)
summaryobjectcounts and estimates (below)
dispositionsobjectthe decisions applied to the needs-work and blocker items (below)
schemasarrayname, kind (user or supabase_managed), target (app for public, the same name for another user schema, null for a managed one), tables
tablesarrayuser tables (below)
viewsarrayschema, name, materialized, references_auth, references_storage
sequencesarrayschema, name, owned_by (table.column or null), data_type
enumsarrayschema, name, labels (in sort order)
extensionsarrayname, version, schema, support: supported, supabase_platform, unsupported or unknown
functionsarrayschema, name, arguments, kind, language, returns, security_definer, references_auth, references_storage, references_net — never the body
triggersarrayschema, table, name, function (schema.name), definition, enabled; user triggers on auth/storage tables are included
policiesarrayRLS policies on the venture's own tables (below); Supabase-managed and storage policies are elsewhere
managed_policiesarraypolicies on Supabase-managed schemas (auth, cron, vault, storage tables other than objects/buckets), for audit only (below)
api_role_grantsarrayrole (anon, authenticated, service_role) and the tables it holds a privilege on
authobjectSupabase Auth as counts (below)
storageobjectpresent, buckets (below)
edge_functionsobjectstatus and functions (slug, name, status, verify_jwt)
realtimeobjectpublications: name, all_tables, operations, tables
cron_jobsarrayname, schedule, command (scrubbed), active; null when cron.job is not visible, [] only when there is none
findingsarrayevery item classified (below)
warningsarray of stringswhat the reader should know that is not a finding

#read_only

FieldMeaning
transaction_read_onlythe reads ran in a READ ONLY transaction, read back inside it
session_read_onlythe session default was read-only too
no_transaction_id_assignedPostgres never assigned the transaction an id, which a write would have
rolethe role inspect connected as
role_is_superuserit is a superuser
role_can_writeit holds INSERT/UPDATE/DELETE/TRUNCATE on a user, auth or storage table, or CREATE on one of those schemas

#coverage.sections[]

One entry per row-read section: auth, storage.buckets, storage.objects, cron (only when cron.job exists) and schema:<name> for each user schema.

FieldMeaning
sectionauth, storage.buckets, storage.objects, cron or schema:<name>
coverageinspected or not_visible
reasonwhy it is not visible, ; -joined; omitted from the JSON when empty
fixthe SQL that would make it visible, one line each, ---commented alternatives included; omitted when there is none

not_visible means the role lacks a privilege the read needs — USAGE, SELECT, or a way past row-level security. The section's data is then null, never 0 or []: a wrong zero would be believed, an absent fact cannot. Each not_visible section also adds a visibility blocker finding carrying the same reason and fix, so ready is false.

#summary

FieldMeaning
automatic, needs_work, blockersfinding counts
decided, undecidedneeds-work and blocker items with, and without, a disposition; cutover refuses while undecided is non-zero (#661)
readyno blockers; since a not_visible section adds one, false while any section is not_visible
tables, estimated_rowsuser tables and the sum of their planner estimates
data_bytesheap and TOAST bytes of the user tables: what the data step moves
index_bytestheir index bytes: rebuilt on the target, not moved
storage_objects, storage_bytesfrom storage.objects and each object's recorded size; null when the buckets or their object counts are not visible
transfer_assumed_mbpsthe throughput the estimate assumes
estimated_transfer_seconds(data_bytes + storage_bytes) at that throughput, rounded up, or data-only when storage_bytes is null; index builds and verification are not in it

#tables[]

schema, name, kind (table or partitioned_table), estimated_rows (null when never analyzed), data_bytes, index_bytes, rls_enabled, rls_forced, primary_key (column names in key order; empty when none), columns (name, data_type, nullable, default, identity — always/by_default/null — and generated), constraints (name, kind — primary_key, foreign_key, unique, check, exclusion, trigger — definition, and references as schema.table for a foreign key), indexes (name, unique, primary, definition). Tables owned by an extension (PostGIS's spatial reference table) are not listed.

#policies[]

FieldMeaning
schema, table, namethe policy
commandALL, SELECT, INSERT, UPDATE or DELETE
permissivepermissive or restrictive
rolessorted
using, with_checkthe expressions as Postgres renders them, scrubbed; null when absent
patternowner_only, tenant_scoped, public_read, public_write, role_based, service_role_only, owner_via_parent, tenant_via_parent, public_read_filtered, deny_all, composite, custom_logic or needs_review
confidence1.0 for a rule match, the classifier's own number for a classifier label, 0.0 when unplaced
sourcerule or classifier
classifier_labelthe label the classifier gave below the threshold (the policy is then needs_review); otherwise null
suggested_equivalentthe check to write in a route — advice, never enforcement code
test_stuba failing Rust test (todo!()) naming the policy and quoting its SQL, to become the test that proves the replacement
dispositioncovered, waived or undecided: this item's entry in the dispositions file (ADR 0026, Decision 5); undecided in an inspect report with no file

#dispositions

fz import supabase inspect --dispositions <FILE> applies a TOML file of per-item decisions (ADR 0026, Decision 5; docs/import/supabase.md). Without one, every needs-work and blocker item is undecided: decided and stale are empty and by_kind lists them all as undecided.

FieldMeaning
by_kindper finding kind: covered, waived and undecided counts; only kinds with an item, sorted by kind
decidedone entry per decided item — id, status (covered or waived), and ref (covered only) or reason (waived only) — sorted by id
staleentry ids from the file that match no needs-work or blocker item, sorted

A policy on a Supabase-managed schema never appears here. One on storage.objects or storage.buckets is attached to its bucket instead (see storage). Everything else on a managed schema is in managed_policies.

#managed_policies[]

schema, table, name, command, roles (sorted), using, with_check. Policies on Supabase's own schemas (auth, cron, vault, net, storage tables other than objects/buckets, …) are the platform's: audited, but never classified, copied or a finding — so never in summary.needs_work.

#auth

present, users, users_without_password, users_unconfirmed, anonymous_users, identities_by_provider (provider, identities), mfa_factors, sso_providers, and from the Management API enabled_providers and enabled_mfa (null when not inspected). A count is null, never 0, when the role cannot see auth.users or auth.identities.

#storage

present; buckets: id, name, public, file_size_limit, allowed_mime_types, objects, bytes, objects_over_blob_cap (objects over the Blob port's 10 MiB put cap), and policies; plus unattached_policies. buckets is null, never [], and a bucket's objects, bytes and objects_over_blob_cap are each null, when the role cannot see storage.buckets or storage.objects.

Each bucket's policies holds the storage.objects/storage.buckets policies that name it (or, with all_buckets, apply to every bucket): table (storage.objects or storage.buckets), name, command, roles (sorted), using, with_check, all_buckets (the expression named no bucket that exists, so the policy applies to every bucket — and to any added later), suggested_equivalent (a route that checks the expression and hands out a signed URL, or a public URL for a public read — advice, never enforcement code) and disposition (undecided; per policy before cutover, ADR 0026 Decision 5). Bucket ids are parsed as pg_policies renders them (bucket_id = 'x', 'x'::text = bucket_id, or bucket_id = ANY (ARRAY['a', 'b']); on storage.buckets the column is id or name). unattached_policies holds the same shape when the project has no buckets at all, or the bucket list is not visible, where there is nothing to attach them to.

#findings[]

FieldMeaning
id<kind>:<object>, stable across runs; the sort key
kindschema, table, rls_no_policy, foreign_key, view, materialized_view, sequence, enum, extension, function, trigger, policy, storage_policy, grant, auth, auth_provider, bucket, edge_function, publication, cron_job, visibility
objectthe qualified name
classificationautomatic, needs_work or blocker
phasethe step that handles it — schema, data, auth, storage — or code when the venture's own code has to
reasonwhy it is classified so
cratefield_equivalentwhat it becomes on Cratefield

A later step looks up its own items by kind and phase; the dashboard groups by classification. Every needs_work finding needs a disposition before cutover (ADR 0026, Decision 5).

rls_no_policy is automatic, phase code: a table with RLS enabled and no policy denies every client, so nothing is ported (its cratefield_equivalent says service_role_only); it does not inflate needs_work. storage_policy is needs_work, phase storage: one finding per storage.objects / storage.buckets policy however many buckets it landed on, its reason naming the bucket(s). Such a policy is never in policies and never a phase-code finding; the policy itself is on its bucket (or in unattached_policies).

Mirrored from docs/import/supabase-report.md at b6f50cd. The harness is the source of truth: a correction goes there first.