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_versionis2. It changes when a field is removed or renamed, or changes meaning —1→2(issue #723) because a section the role cannot see isnullwhere it used to be0, andstorage.counts_exactis 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_casestrings. - Nothing secret is in it: no credential, no password hash, no email, no row data. The connection is named by
host,portanddatabasealone, 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
nullwhen it was not inspected or the role cannot see it — never a0or an empty list, which would claim a fact the report does not have.
#Top level
| Field | Type | Meaning |
|---|---|---|
report_version | integer | 2 |
tool | object | name, version of the crate that wrote it |
project | object | ref, host, port, database, server_version |
read_only | object | what kept the inspection from writing, as observed (below) |
coverage | object | database, management_api, policy_classifier (each inspected, not_inspected, not_visible or failed) and sections (below) |
summary | object | counts and estimates (below) |
dispositions | object | the decisions applied to the needs-work and blocker items (below) |
schemas | array | name, kind (user or supabase_managed), target (app for public, the same name for another user schema, null for a managed one), tables |
tables | array | user tables (below) |
views | array | schema, name, materialized, references_auth, references_storage |
sequences | array | schema, name, owned_by (table.column or null), data_type |
enums | array | schema, name, labels (in sort order) |
extensions | array | name, version, schema, support: supported, supabase_platform, unsupported or unknown |
functions | array | schema, name, arguments, kind, language, returns, security_definer, references_auth, references_storage, references_net — never the body |
triggers | array | schema, table, name, function (schema.name), definition, enabled; user triggers on auth/storage tables are included |
policies | array | RLS policies on the venture's own tables (below); Supabase-managed and storage policies are elsewhere |
managed_policies | array | policies on Supabase-managed schemas (auth, cron, vault, storage tables other than objects/buckets), for audit only (below) |
api_role_grants | array | role (anon, authenticated, service_role) and the tables it holds a privilege on |
auth | object | Supabase Auth as counts (below) |
storage | object | present, buckets (below) |
edge_functions | object | status and functions (slug, name, status, verify_jwt) |
realtime | object | publications: name, all_tables, operations, tables |
cron_jobs | array | name, schedule, command (scrubbed), active; null when cron.job is not visible, [] only when there is none |
findings | array | every item classified (below) |
warnings | array of strings | what the reader should know that is not a finding |
#read_only
| Field | Meaning |
|---|---|
transaction_read_only | the reads ran in a READ ONLY transaction, read back inside it |
session_read_only | the session default was read-only too |
no_transaction_id_assigned | Postgres never assigned the transaction an id, which a write would have |
role | the role inspect connected as |
role_is_superuser | it is a superuser |
role_can_write | it 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.
| Field | Meaning |
|---|---|
section | auth, storage.buckets, storage.objects, cron or schema:<name> |
coverage | inspected or not_visible |
reason | why it is not visible, ; -joined; omitted from the JSON when empty |
fix | the 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
| Field | Meaning |
|---|---|
automatic, needs_work, blockers | finding counts |
decided, undecided | needs-work and blocker items with, and without, a disposition; cutover refuses while undecided is non-zero (#661) |
ready | no blockers; since a not_visible section adds one, false while any section is not_visible |
tables, estimated_rows | user tables and the sum of their planner estimates |
data_bytes | heap and TOAST bytes of the user tables: what the data step moves |
index_bytes | their index bytes: rebuilt on the target, not moved |
storage_objects, storage_bytes | from storage.objects and each object's recorded size; null when the buckets or their object counts are not visible |
transfer_assumed_mbps | the 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[]
| Field | Meaning |
|---|---|
schema, table, name | the policy |
command | ALL, SELECT, INSERT, UPDATE or DELETE |
permissive | permissive or restrictive |
roles | sorted |
using, with_check | the expressions as Postgres renders them, scrubbed; null when absent |
pattern | owner_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 |
confidence | 1.0 for a rule match, the classifier's own number for a classifier label, 0.0 when unplaced |
source | rule or classifier |
classifier_label | the label the classifier gave below the threshold (the policy is then needs_review); otherwise null |
suggested_equivalent | the check to write in a route — advice, never enforcement code |
test_stub | a failing Rust test (todo!()) naming the policy and quoting its SQL, to become the test that proves the replacement |
disposition | covered, 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.
| Field | Meaning |
|---|---|
by_kind | per finding kind: covered, waived and undecided counts; only kinds with an item, sorted by kind |
decided | one entry per decided item — id, status (covered or waived), and ref (covered only) or reason (waived only) — sorted by id |
stale | entry 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[]
| Field | Meaning |
|---|---|
id | <kind>:<object>, stable across runs; the sort key |
kind | schema, 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 |
object | the qualified name |
classification | automatic, needs_work or blocker |
phase | the step that handles it — schema, data, auth, storage — or code when the venture's own code has to |
reason | why it is classified so |
cratefield_equivalent | what 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.