Reviewing an AI-built Supabase application before production
Contents
For a Supabase application, a production review needs to trace identity, authorization and data integrity across the browser, Data API, PostgreSQL functions, Edge Functions and Storage. Reviewing generated code in isolation will miss failures created by the interaction between those components.
The same standards apply whether the implementation was written manually or generated with AI. With an iteratively prompted application, particular attention belongs on accumulated changes: temporary policies, duplicated authorization logic, direct dashboard edits and privileged workarounds introduced to resolve earlier errors.
Establish the access model before reviewing policies
Start by documenting the application's principals, resources and permitted operations. For a multi-tenant application, distinguish an authenticated user from an organization member, an organization administrator and a platform administrator. These are different authorization contexts.
For each resource, establish who may read it, create it, modify it, delete it and change its ownership. Specify whether access depends on ownership, current membership, a delegated permission or a public visibility state.
This produces concrete invariants. For example: a project belongs to exactly one organization; a member can read projects only in organizations where their membership is active; only designated administrators can change membership roles.
Those invariants become the basis for reviewing database policies, function implementations and tests. Without them, a policy can be syntactically correct while enforcing the wrong business rule.
Inventory the actual deployment alongside the repository. Include exposed schemas, object grants, policies, views, callable functions, function owners, Storage buckets, scheduled jobs and credentials. A migration directory is insufficient evidence if production has also been modified through the SQL Editor.
Review grants and RLS as separate controls
Supabase's Data API relies on PostgreSQL privileges and Row Level Security. Grants determine whether a role can perform an operation on an object; RLS determines which rows it may access for operations governed by row policies. Both need explicit review. [1]
A frontend filter such as .eq('organization_id', activeOrganizationId) expresses the client's requested scope. The server must enforce the permitted scope independently, including when the filter is removed or replaced.
An RLS review should cover each operation. USING governs access to existing rows; WITH CHECK governs proposed rows for inserts and updates. An update policy must account for both the row being targeted and the resulting ownership or tenant assignment.
Review policies together. PostgreSQL combines applicable permissive policies with OR and restrictive policies with AND, with a permissive policy required to grant access. An additional permissive policy can therefore widen access that another policy appears to constrain. [2]
RLS also does not provide column-level authorization. Permission to update a profile row should not automatically imply permission to modify its administrative flags, billing entitlements or verification status. Review column grants and separate privileged state from fields the user may edit.
Public read access may be intentional. The requirement is to establish which fields and records are public and verify that the available write paths cannot expand that scope.
Trace the effective identity through Edge Functions
An Edge Function executes on the server, but its security depends on the credentials it accepts, the identity it validates and the database role used for downstream operations.
Supabase distinguishes publishable keys from secret keys and legacy anon and service_role keys. A publishable key is intended for client applications; its presence in a browser bundle is expected. Secret and service-role credentials belong in trusted infrastructure. Requests executing as service_role bypass RLS. [3]
Inspect the effective authorization context of each database request. A client initialized with a privileged key may still execute under user policies when a user access token is supplied. Conversely, an authenticated HTTP request does not automatically make a separate administrative database client user-scoped.
Consider an export endpoint that validates a user's token, accepts an organization_id from the request body and queries that organization using a privileged client. Token validation establishes the caller's identity. The handler must still establish that this caller may export the requested organization's data.
Review every transition from user input to privileged access. Derive identity from verified credentials, validate resource permissions and restrict writable fields. Treat submitted ownership identifiers, roles and entitlement values as untrusted input.
Authentication configuration also requires deployment-specific inspection. Supabase supports different patterns for user requests, internal services and public endpoints. Review gateway settings and handler verification together; the name or presence of a JWT setting does not establish resource authorization. [4]
CORS controls browser cross-origin behavior. An allowed origin is not evidence that a request is authorized.
Inspect database functions and views for privilege changes
Database functions and views can introduce access paths that differ from direct table queries.
A SECURITY DEFINER function executes with its owner's privileges. If that owner bypasses RLS, the function may access rows unavailable to its caller. That can be appropriate for a narrowly defined operation, but its implementation must enforce the intended boundary. [5]
Review the owner's privileges, execution grants, input validation, dynamic SQL, object references and authorization checks. Set a controlled search_path and qualify referenced objects appropriately. PostgreSQL grants function execution to PUBLIC by default; review inherited and explicit grants and restrict execution when the function is created. A safe search path does not restrict who can invoke a function. [5]
Views require a separate check. By default, underlying table permissions are checked using the view owner's privileges. A view owned by a privileged role can consequently expose data that direct table access would withhold. Where caller permissions and underlying RLS should apply, review the use of security_invoker = true, supported from PostgreSQL 15. [6]
An authorization review must follow every exposed route to the data.
Enforce tenant consistency in the schema
Authorization determines whether an actor may perform an operation. Constraints determine whether the resulting data is valid.
Consider a task containing both organization_id and project_id. A foreign key from project_id to projects.id proves that the project exists. It does not prove that the task and project belong to the same organization.
Where that relationship is required, one option is a composite foreign key from tasks(organization_id, project_id) to a corresponding unique key on projects(organization_id, id), with the required nullability constraints. This makes the tenant relationship enforceable regardless of which write path performs the insert. [7]
Apply the same reasoning to duplicate memberships, external identifiers, mandatory ownership and mutually exclusive relationships. Identify each invariant and enforce it at the appropriate layer.
Do not assume an arbitrary cross-table business rule can be implemented safely with a CHECK constraint. PostgreSQL does not support such constraints as a general mechanism for validating other rows. Foreign keys, unique or exclusion constraints, and carefully designed transactional operations serve different purposes. [7]
Test concurrent writes and repeated delivery
A sequential test session does not exercise contention.
Suppose an endpoint reads a usage counter, verifies that it is below a limit and then increments it. Concurrent requests can both pass the check before either records its consumption. Wrapping the sequence in a transaction does not automatically resolve the race: PostgreSQL's default Read Committed isolation allows anomalies across successive statements. [8]
Choose concurrency controls around the invariant. Depending on the operation, that may mean an atomic conditional update, a unique constraint, explicit row locking or Serializable isolation with transaction retries.
External side effects need separate treatment. A committed database transaction cannot roll back an email already sent or a successful third-party API call. A timed-out request may have completed remotely before the caller retries.
For webhook and job processing, review delivery identifiers, idempotency keys, duplicate handling and recovery after partial failure. Where appropriate, an outbox can record pending work in the same transaction as the application change, with a worker delivering it afterward.
The design must specify what happens when a request repeats or stops halfway through.
Include Storage and downstream data copies
A protected database row does not protect a separately accessible file. Supabase Storage has its own access policies, and public and private bucket behavior must be reviewed independently. [9]
Verify upload, retrieval, listing, replacement and deletion permissions. A tenant identifier embedded in an object path must be checked against actual authorization. A signed URL also creates a temporary access capability whose issuance and lifetime need to match the sensitivity of the file.
Follow the data beyond its primary table: exports, generated documents, logs, caches and search indexes can preserve information after the original permissions change.
For AI features, apply authorization before retrieving context for the model. Tenant isolation belongs in retrieval, tool execution and cache design. A prompt instructing the model to respect permissions cannot enforce database access.
Require evidence from negative tests
A useful review produces reproducible findings with an affected access path, required privileges, business impact and a regression test.
Authorization tests should run under representative client roles and credentials. Queries executed as a database owner or through a privileged client do not demonstrate that user-facing RLS works.
At minimum, test unauthenticated access, valid access, another user's records, another tenant's records, ownership reassignment, role escalation and access after membership removal. Exercise direct Data API requests, RPC calls, Edge Functions and Storage operations where those paths exist.
Assert the observable result. An unauthorized read may correctly return zero rows rather than an explicit permission error. An unauthorized update test should verify that protected state remained unchanged.
Combine database tests with HTTP integration tests. Database tests establish policy and constraint behavior; integration tests establish whether deployed handlers preserve the intended identity and authorization context.
Define a concrete release decision
A production review should deliver a prioritized remediation plan, reviewed migrations and regression coverage. Findings should distinguish immediately exploitable authorization failures from integrity defects, operational gaps and maintainability concerns.
Reproduce the schema and security configuration from version-controlled migrations. Verify recovery procedures, inspect production query plans under representative roles and establish monitoring that can identify failed operations without collecting unnecessary sensitive payloads.
The release decision should state which boundaries were tested, which findings were resolved and what remains outside the review's scope. A clean dashboard or an automated scan cannot supply that evidence on its own.
For an AI-built Supabase application, the essential engineering task is to make its assumptions explicit and enforce them consistently across every exposed execution path.
Have an existing Supabase build that needs a technical review? Pinkdot can review its database structure, RLS policies, Edge Functions and integrations, then define the work required before launch.