Docs Supabase report, worked example

DocsReference

#Supabase migration report: fixtureprojectref000

Source db.fixtureprojectref000.supabase.co:5432/postgres, PostgreSQL 16.15. Report version 2, written by cratefield-import-supabase 0.1.0. Read-only: nothing was written to the project.

#Summary

Ready (no blockers)no
Automatic23
Needs work28
Blockers2
Decided0
Undecided28
Tables6 (~14 rows)
Data88 KiB (plus 112 KiB of indexes, rebuilt on the target)
Storage3 objects, 12 MiB
Estimated transferabout 2 s at 100 Mbit/s (data and storage only; index builds and verification are extra)

Read-only evidence: read-only transaction yes, read-only session yes, no transaction id assigned yes; role postgres (superuser yes, holds write privileges yes).

Coverage: database inspected, Management API not inspected, policy classifier not inspected.

#Blockers (2)

Each stops a later step until it is resolved.

KindObjectPhaseWhyCratefield equivalent
extensiondblinkschemacross-database queries: the harness is one database per tenant (ADR 0008) and reaches other systems through ports, not through SQLnone
foreign_keypublic.attachments.attachments_object_id_fkeyschemapoints into storage.objects, which no phase recreates as a table (storage objects become R2 keys, auth rows become harness users)drop the constraint on the source or decide what the column holds after the move (an object key, a user id) before the schema phase

#Needs work (28)

Moved or reported, but the venture writes or decides something. Each needs a covered or waived disposition before cutover.

KindObjectPhaseWhyCratefield equivalent
authmfa_factorsauthMFA factors are not imported; enrolled users lose their second factorre-enrolment (passkeys, auth-passkeys) after the move
auth_providergithubauthgithub sign-in has no harness providerits users sign in by magic link until a provider is added
auth_providergoogleauthgoogle sign-in: identities map once the provider is configuredauth-oidc's google provider
bucketdocumentsstorage1 object(s) over the Blob port's 10 MiB put cap: copied and readable, but module code cannot replace them through the portR2 under the same keys
edge_functionunknowncodeEdge Functions are only visible through the Management API, which was not read: unknown, not nonere-run with a Management API token
foreign_keypublic.profiles.profiles_id_fkeyschemapoints into auth.users: dropped on load, its values kept and checked against the imported users in verifythe imported user's id, mapped through the auth-users step's id table
foreign_keypublic.team_members.team_members_user_id_fkeyschemapoints into auth.users: dropped on load, its values kept and checked against the imported users in verifythe imported user's id, mapped through the auth-users step's id table
functionpublic.handle_new_user()codeSECURITY DEFINER: it bypasses the caller's privilegesmodule code over the request's subject (and the HttpClient port for HTTP); or copy it once it no longer reaches into Supabase
grantanon (8 tables)codea PostgREST role: the harness exposes no table directly, so what this role could reach is reachable only through a route someone writesa route per access path, with its check
grantauthenticated (8 tables)codea PostgREST role: the harness exposes no table directly, so what this role could reach is reachable only through a route someone writesa route per access path, with its check
policypublic.attachments.Anyone can file an attachmentcodeRLS is neither copied nor translated (ADR 0026); public_write by rule at confidence 1.00; needs a covered or waived disposition before cutovera public write route: rate-limit it and put the Captcha port in front; confirm it is meant to be open
policypublic.profiles.Public profiles are viewable by everyone.codeRLS is neither copied nor translated (ADR 0026); public_read by rule at confidence 1.00; needs a covered or waived disposition before cutovera public read route with no subject check; confirm every column is meant to be public
policypublic.profiles.Users can insert their own profile.codeRLS is neither copied nor translated (ADR 0026); owner_only by rule at confidence 1.00; needs a covered or waived disposition before cutoverowner check in the route: the request's subject must equal id (filter reads and refuse writes by it)
policypublic.profiles.Users can update own profile.codeRLS is neither copied nor translated (ADR 0026); owner_only by rule at confidence 1.00; needs a covered or waived disposition before cutoverowner check in the route: the request's subject must equal id (filter reads and refuse writes by it)
policypublic.projects.Admins can delete projectscodeRLS is neither copied nor translated (ADR 0026); role_based by rule at confidence 1.00; needs a covered or waived disposition before cutovera role or claim check in the route on the request's subject (signed in, or holding the role)
policypublic.projects.Archived projects stay visible for a grace periodcodeRLS is neither copied nor translated (ADR 0026); needs_review by rule at confidence 0.00; needs a covered or waived disposition before cutoverread the expression and write the equivalent route check, with a failing test first
policypublic.projects.Members can read their team's rowscodeRLS is neither copied nor translated (ADR 0026); needs_review by rule at confidence 0.00; needs a covered or waived disposition before cutoverread the expression and write the equivalent route check, with a failing test first
policypublic.projects.Team members can view projectscodeRLS is neither copied nor translated (ADR 0026); tenant_scoped by rule at confidence 1.00; needs a covered or waived disposition before cutovermembership check in the route: the request's subject must be a member, looked up in team_members, of the row's tenant
policypublic.team_members.Members can leave unless they own the teamcodeRLS is neither copied nor translated (ADR 0026); needs_review by rule at confidence 0.00; needs a covered or waived disposition before cutoverread the expression and write the equivalent route check, with a failing test first
policypublic.team_members.Members see their membershipscodeRLS is neither copied nor translated (ADR 0026); owner_only by rule at confidence 1.00; needs a covered or waived disposition before cutoverowner check in the route: the request's subject must equal user_id (filter reads and refuse writes by it)
policypublic.teams.Service role manages teamscodeRLS is neither copied nor translated (ADR 0026); service_role_only by rule at confidence 1.00; needs a covered or waived disposition before cutoverserver-side only: no route exposes it; module code reads and writes it directly
policypublic.teams.Signed-in users can list teamscodeRLS is neither copied nor translated (ADR 0026); role_based by rule at confidence 1.00; needs a covered or waived disposition before cutovera role or claim check in the route on the request's subject (signed in, or holding the role)
publicationsupabase_realtimecodeRealtime streams changes to these tables to clientsthe Realtime port: a room the owning module broadcasts to after each write
storage_policystorage.objects.Avatar images are publicly accessiblestorageRLS is neither copied nor translated (ADR 0026); this policy applies to bucket(s) avatars and needs a covered or waived disposition before cutovera public URL: serve the object under the same key from a public route
storage_policystorage.objects.Users upload their own avatarstorageRLS is neither copied nor translated (ADR 0026); this policy applies to bucket(s) avatars and needs a covered or waived disposition before cutoverserve objects through a route that checks that the request's subject equals owner and hands out a signed URL; keep the owner column
tablepublic.audit_logdatano primary key: copied in one piece and checksummed over its sorted rows, so an interrupted copy restarts the table; add a key for chunked, resumable copyapp.audit_log
triggerauth.users.on_auth_user_createdcodefires on auth.users, which the harness replaces: it will never firea module reacting to the harness's account events (sign-up, deletion)
viewpublic.my_projectscodereads auth. or storage., which do not exist on the target; not copieda module query that takes the request's subject as a parameter

#Automatic (23)

Moved by the importer with nothing to decide.

KindObjectPhaseWhyCratefield equivalent
authusersauth3 user(s): email, verified flag, bcrypt hash and created_at move through the harness's user import; passwords keep workingharness users (auth-password, auth-magic-link)
auth_provideremailauthemail and password, or magic linkauth-password and auth-magic-link
bucketavatarsstorageobjects copied through the Storage API and verified by count, size and checksumR2 under the same keys, served by a public route
enumpublic.project_statusschemarecreated with the same labels in the same orderapp.project_status
extensioncitextschemacreated on the target before the schemacreated on the target
extensionpgcryptoschemacreated on the target before the schemacreated on the target
extensionplpgsqlschemacreated on the target before the schemacreated on the target
extensionuuid-osspschemacreated on the target before the schemacreated on the target
functionpublic.project_count(team bigint)schemacopied verbatim, its search_path pinned to the target schemaapp.project_count
functionpublic.set_updated_at()schemacopied verbatim, its search_path pinned to the target schemaapp.set_updated_at
grantservice_role (8 tables)codethe server-side role; module code has the same reachmodule code
rls_no_policypublic.audit_logcodeRLS is enabled with no policy on the table, so every client request is denied; nothing reaches it but the server sideservice_role_only: only server-side module code touches it, expose no route
schemapublicschemaa user schema; owned by the venture, not tracked by harness migrationsthe Postgres schema app in the venture's database
sequencepublic.invoice_number_seqschemarecreated, and set to at least the source's value in verifyapp.invoice_number_seq
sequencepublic.projects_id_seqschemarecreated, and set to at least the source's value in verifyapp.projects_id_seq
sequencepublic.teams_id_seqschemarecreated, and set to at least the source's value in verifyapp.teams_id_seq
tablepublic.attachmentsdatacolumns, constraints and indexes recreated; rows copied with COPY and verified by count and checksumapp.attachments
tablepublic.profilesdatacolumns, constraints and indexes recreated; rows copied with COPY and verified by count and checksumapp.profiles
tablepublic.projectsdatacolumns, constraints and indexes recreated; rows copied with COPY and verified by count and checksumapp.projects
tablepublic.team_membersdatacolumns, constraints and indexes recreated; rows copied with COPY and verified by count and checksumapp.team_members
tablepublic.teamsdatacolumns, constraints and indexes recreated; rows copied with COPY and verified by count and checksumapp.teams
triggerpublic.profiles.profiles_set_updated_atschemacopied verbatim with its functionon app.profiles
viewpublic.active_projectsschemacopied verbatimapp.active_projects

#Dispositions

Every needs-work item needs its own covered or waived entry (ADR 0026, Decision 5): fz import supabase inspect --dispositions <FILE>. Cutover refuses while Undecided is non-zero.

KindCoveredWaivedUndecided
auth001
auth_provider002
bucket001
edge_function001
extension001
foreign_key003
function001
grant002
policy0012
publication001
table001
trigger001
view001

#Row-level security (12 policies)

RLS is neither copied nor translated (ADR 0026). The pattern and the suggested check are advice: a person writes the check, turns the policy's test stub (in the JSON report) into a passing test, and records the policy as covered or waived.

TablePolicyCommandRolesUSINGWITH CHECKPatternConfidenceSourceSuggested check
public.attachmentsAnyone can file an attachmentINSERTanon—truepublic_write1.00rulea public write route: rate-limit it and put the Captcha port in front; confirm it is meant to be open
public.profilesPublic profiles are viewable by everyone.SELECTpublictrue—public_read1.00rulea public read route with no subject check; confirm every column is meant to be public
public.profilesUsers can insert their own profile.INSERTpublic—(( SELECT auth.uid() AS uid) = id)owner_only1.00ruleowner check in the route: the request's subject must equal id (filter reads and refuse writes by it)
public.profilesUsers can update own profile.UPDATEpublic(auth.uid() = id)—owner_only1.00ruleowner check in the route: the request's subject must equal id (filter reads and refuse writes by it)
public.projectsAdmins can delete projectsDELETEpublic(((auth.jwt() -> 'app_metadata'::text) ->> 'role'::text) = 'admin'::text)—role_based1.00rulea role or claim check in the route on the request's subject (signed in, or holding the role)
public.projectsArchived projects stay visible for a grace periodSELECTpublic((status <> 'archived'::project_status) OR (created_at > (now() - '30 days'::interval)))—needs_review0.00ruleread the expression and write the equivalent route check, with a failing test first
public.projectsMembers can read their team's rowsSELECTpublic((team_id > 0) AND (status <> 'archived'::project_status))—needs_review0.00ruleread the expression and write the equivalent route check, with a failing test first
public.projectsTeam members can view projectsSELECTauthenticated(EXISTS ( SELECT 1 FROM team_members m WHERE ((m.team_id = projects.team_id) AND (m.user_id = auth.uid()))))—tenant_scoped1.00rulemembership check in the route: the request's subject must be a member, looked up in team_members, of the row's tenant
public.team_membersMembers can leave unless they own the teamDELETEpublic((user_id = auth.uid()) AND (role <> 'owner'::text))—needs_review0.00ruleread the expression and write the equivalent route check, with a failing test first
public.team_membersMembers see their membershipsSELECTpublic(user_id = auth.uid())—owner_only1.00ruleowner check in the route: the request's subject must equal user_id (filter reads and refuse writes by it)
public.teamsService role manages teamsALLservice_roletruetrueservice_role_only1.00ruleserver-side only: no route exposes it; module code reads and writes it directly
public.teamsSigned-in users can list teamsSELECTauthenticatedtrue—role_based1.00rulea role or claim check in the route on the request's subject (signed in, or holding the role)

#Supabase-managed policies (0)

None.

#Tables (6)

TableRows (est.)DataIndexesRLSPrimary key
public.attachments18.0 KiB16 KiByesid
public.audit_log216 KiB0 Byesnone
public.profiles216 KiB32 KiByesid
public.projects416 KiB32 KiByesid
public.team_members316 KiB16 KiByesteam_id, user_id
public.teams216 KiB16 KiByesid

#Auth

3 users (2 without a password, 1 unconfirmed, 0 anonymous); 1 MFA factors; 0 SSO providers.

Identities by provider: email (1), github (1), google (1).

The auth configuration was not read (no Management API token).

#Storage

BucketPublicObjectsSizeOver 10 MiB
avatarsyes251 KiB0
documentsno112 MiB1

Bucket avatars policies:

  • Avatar images are publicly accessible (public, SELECT on storage.objects) — a public URL: serve the object under the same key from a public route
  • Users upload their own avatar (authenticated, INSERT on storage.objects) — serve objects through a route that checks that the request's subject equals owner and hands out a signed URL; keep the owner column

#Edge Functions

Not inspected: unknown, not none. Pass a Management API token to list them.

#Realtime and cron

  • Publication supabase_realtime: public.projects.

#Warnings

  • inspected as the superuser postgres: the session was read-only, but use the read-only role from docs/import/supabase.md for the real run

#Next

Resolve the blockers, decide each needs-work item, then run the later steps (docs/import/supabase.md): auth users, schema and data into app, storage into R2, verify, cut over.

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