Skip to content

Database boundaries

Supabase imports, RLS and tenant-owned fields

Separate CSV values from tenant authority. Understand user JWTs, service-role access and the PostgreSQL COPY limitation when RLS applies.

Mohamed Atef — Founder, ImportFlow
Last reviewed

Take the import’s tenant from an authenticated, authorized application decision. Exclude it from the customer’s column mapping. Then enforce the allowed write at the destination under the role that will actually execute it.

Enabling RLS is only part of that design. A Supabase API request carrying a user JWT, a server request using service-role authority, and a database-owner psql session do not have the same permissions. An import working in the dashboard does not demonstrate that it will work through a customer’s session—or that tenant isolation was exercised.

If you only need the upload steps, start with importing CSV into Supabase. This page addresses the authority boundary behind those writes.

Scope and assumptions. PostgreSQL 17 and Supabase Auth/Data API behavior. Policy examples are illustrative and require the stated membership and grant model.

Identify the execution path first

PathAuthority to inspectWhat a successful write proves
Data API with a signed-in user JWTEffective authenticated role, JWT claims, grants and policiesThe request passed the applicable checks at that time
Backend using service-role authorizationPrivileged role and the backend’s own authorizationDo not infer RLS enforcement; service_role has BYPASSRLS
Dashboard / privileged database connectionActual session role, ownership and bypass attributesNot evidence of ordinary application-user access
Customer-controlled scoped database loginRole grants, table ownership, applicable RLS and session contextOnly the permissions and checks exercised by that login

Supabase’s API key guide distinguishes publishable/anon access from secret/service-role access. Its RLS guide also explains an important client detail: a client initialized with a service key can follow a signed-in user’s RLS context when its authorization is replaced by that user’s session. Inspect the effective Authorization credential, not only the key used at construction.

In PostgreSQL, superusers and roles with BYPASSRLS bypass row security. Table owners normally bypass it too, unless FORCE ROW LEVEL SECURITY applies to the owner. FORCE does not constrain a superuser or a BYPASSRLS role. This is why testing only as the table owner is insufficient.

Grants and RLS solve different problems. A role needs the relevant table or column privilege; its row policy must also permit the operation when RLS applies. If no applicable policy permits access, an RLS-enabled table uses default deny for ordinary roles. See PostgreSQL’s row-security rules.

Bind one import to an authorized tenant

Consider a founder importing contacts for a workspace. Their CSV contains account_id from a previous CRM. Your database also has an account identifier. The old value might be useful as an external reference, but it is not evidence that the importer may write into the corresponding workspace in your system.

The application should authenticate the operator, check their import permission for the selected workspace, and bind that workspace to the import job. The approved row mapping then contains only fields such as email and display name. The writer supplies tenant context independently.

Illustrative server pseudocode
subject = authenticate(request)
tenant = authorizeImport(subject, selectedWorkspace)
job = createJob({ tenant, actor: subject.id, contractVersion })

for each submitted row:
  reject keys outside the approved source fields
  validate source values again on the server
  write approved values with tenant = job.tenant

Authentication alone is not authorization. A user can be signed in without belonging to the selected workspace, or belong to it without permission to import. Check the permission for this operation and define how revocation affects queued work. A long-running job should not silently gain indefinite authority from a session that was valid yesterday.

Do not use editable user metadata as a tenant authority source. If you use JWT claims, establish who issues them, who can change them and how stale membership is handled. A signed claim is only as useful as the trusted process that created it.

Use WITH CHECK to constrain new rows

The following policy illustrates an application where a trusted membership table records who may import into each tenant. Assume public.contacts(tenant_id uuid, email text) exists with both columns NOT NULL. Assume public.tenant_memberships(tenant_id uuid, user_id uuid, can_import boolean) is protected against user-created grants, and its SELECT permissions/policies let a user read their own memberships.

SQL
ALTER TABLE public.contacts ENABLE ROW LEVEL SECURITY;

CREATE POLICY contacts_member_import
ON public.contacts
FOR INSERT TO authenticated
WITH CHECK (
  EXISTS (
    SELECT 1
    FROM public.tenant_memberships AS membership
    WHERE membership.tenant_id = contacts.tenant_id
      AND membership.user_id = (SELECT auth.uid())
      AND membership.can_import
  )
);

This is a policy fragment, not a complete installation. The role also needs suitable INSERT privileges. If the request returns rows, review the SELECT privileges and policies required by that response. An INSERT policy uses WITH CHECK to test the proposed row; a SELECT policy does not substitute for it.

Review every existing policy. Permissive policies generally combine with OR, so adding this policy beside a broad permissive INSERT policy may not narrow access. Restrictive policies combine differently and need an applicable permissive policy. Evaluate the complete policy set rather than treating one good-looking expression as the whole boundary.

This membership check permits an authorized tenant; it does not bind a request to one previously selected job tenant. The writer must still reject file-controlled tenant fields and use the job’s authorized value. If the threat model includes bypassing that writer, design a destination-enforced job boundary as well. Do not present this fragment as providing that stronger property.

COPY is not a user-RLS import shortcut

PostgreSQL 17 does not support COPY FROM when RLS applies to the executing role. The documentation recommends equivalent INSERT statements. Loading a temporary staging table and then using an authorized INSERT into the target can be a suitable design, provided the session, row values, grants and policies are reviewed.

A table owner or bypass role can behave differently because row security may not apply to that role. That is not a workaround that preserves the original user-policy guarantee. It is a privileged migration path whose tenant checks and operational review must be established separately.

Direct SQL connections also do not automatically carry a Supabase user’s JWT context. A function such as auth.uid() relies on request/session claims being available through the relevant path. Do not assume a psql login represents the end user just because it connects to the same database.

For a one-time buyer-operated migration, privileged execution may be a deliberate choice. Have the buyer’s engineer review the source-to-tenant assignment, exact columns, target and resulting rows. Keep that procedure distinct from an end-user importer enforced by RLS. The COPY documentation describes the database limitation.

Test the forbidden writes, not just the successful one

Use isolated test tenants and synthetic rows. Run tests through the same role and request path as the real importer, with the same grants and complete policy set.

  • A user with import permission for tenant A can create an approved row in A.
  • A user with import permission only for tenant A is rejected when requesting B.
  • For a job already bound to A, a supplied tenant B is rejected even if the user separately has permission for B. This requires job-level enforcement in addition to the membership policy.
  • A member without import permission is rejected.
  • An unauthenticated or expired session does not acquire a fallback privileged write path.
  • A supplied tenant field is rejected even if it happens to equal the correct tenant.
  • A queued job follows the documented rule after membership is revoked.

After success, read the row through the normal application path for A and B. Confirm visibility as well as insertion. Keep errors from disclosing another tenant’s identifiers or data. If a test succeeds only as the owner, you have tested owner authority, not the intended customer path.

A foreign key from a contact to a company also deserves review: a globally valid company ID might belong to another tenant. Use an appropriate tenant-scoped reference constraint and resolution strategy. The foreign-key guide develops that example.

How this relates to ImportFlow

ImportFlow’s public ADR-001 records the decision against vendor custody of production service-role credentials. Its customer-hosted gateway and runtime enforcement are planned architecture.

Today’s founder-assisted pilot prepares one table with sanitized or synthetic material in local staging. The buyer’s engineer reviews and executes the final production COPY in the buyer’s environment. That human-reviewed migration boundary is not a deployed user-JWT importer.

Sources and further reading