Skip to content

Top 32 Supabase Interview Questions and Answers [2026]

November 12, 2026 29 min read
Supabase interview guide by Second Talent
TL;DR: Supabase interviews in 2026 test whether a candidate can secure a Postgres database that the browser talks to directly: RLS policies that hold up, the right API key in the right place, and migrations that reach production through CI. Candidates should know Supabase CLI v3.0.0 (November 2, 2026), Postgres 17 as the platform default, the publishable and secret keys that replace anon and service_role by the end of 2026, and the October 30, 2026 change that stopped exposing new tables to the Data API automatically.

Since October 30, 2026, a new table in the public schema of any Supabase project is invisible to the Data API until someone runs an explicit GRANT, which ends the default that made every new table reachable from the browser the moment it was created.

The changelog entry lists the staged rollout, and Supabase CLI v3.0.0 shipped three days later with local-by-default commands.

The questions below are for interviews with full-stack and backend engineers who build on Supabase, with three find-the-bug rounds.

Key takeaways
  1. 1A secret key (sb_secret_...) maps to service_role, which has BYPASSRLS. One leaked secret key exposes every row in the project.
  2. 2Since October 30, 2026, new public tables need an explicit GRANT before anon or authenticated can reach them through the Data API.
  3. 3In CLI v3, a bare supabase db push targets the local stack. CI jobs need --remote and --yes or they succeed without touching production.
  4. 4Edge Functions get 2 seconds of CPU time per request and 256MB of memory, with a 150s wall clock on Free and 400s on paid plans.

Postgres, RLS and the Data API

1. How does a request from supabase-js reach Postgres, and what decides what it can see?

The client sends an HTTP request to the project's Data API, which is PostgREST.

PostgREST reads the API key from the apikey header and the user's session JWT from Authorization, switches to the matching Postgres role (anon, authenticated or service_role), sets the JWT claims for the transaction, and runs the query as that role.

Two layers then decide access. Table privileges (GRANT) decide whether the role can touch the table at all. Row Level Security policies decide which rows it can read or change.

A strong candidate names both layers and says that the browser is a direct database client, so the policies are the backend. A weak answer treats the client library as an ORM sitting behind some server they wrote.

2. What does enabling RLS on a table do before you write any policy?

It denies everything. With RLS enabled and no policies, anon and authenticated get zero rows on select and every write fails, because Postgres treats a missing policy as "no rows pass".

Roles with BYPASSRLS, such as service_role and the table owner postgres, are not affected.

The mistake interviewers want candidates to spot is the opposite case: a table in an exposed schema with RLS disabled is readable and writable by anyone holding the publishable key, as long as the role has table grants.

The Security Advisor in the dashboard flags these tables. A senior candidate enables RLS in the same migration that creates the table.

create table public.notes (
  id bigint generated always as identity primary key,
  user_id uuid not null references auth.users (id) on delete cascade,
  body text not null,
  created_at timestamptz not null default now()
);

alter table public.notes enable row level security;

grant select, insert, update, delete on public.notes to authenticated;

create policy "Owners read their notes"
on public.notes for select
to authenticated
using ( (select auth.uid()) = user_id );

3. What is the difference between USING and WITH CHECK?

USING filters the existing rows a statement can see or target. WITH CHECK validates the new row a statement is trying to write.

Select and delete policies take only USING, insert policies take only WITH CHECK, and update policies take both: USING picks which rows the user may update, WITH CHECK decides what those rows may become.

An update policy with USING and no WITH CHECK makes Postgres reuse the USING expression for the new row.

That default is safe for ownership checks, but a candidate should still write both out, because question 7 shows how an update policy without a proper check lets a user move a row to another owner.

4. Your RLS-protected list endpoint got slow as the table grew. What do you check?

Run explain analyze on the query with RLS active and look for a sequential scan with a large "Rows Removed by Filter" count. The RLS guide lists the fixes a candidate should name:

  • Index the policy columns. A btree index on user_id lets the policy filter use an index scan.
  • Wrap function calls in a select. (select auth.uid()) lets Postgres evaluate it once per statement as an initPlan instead of once per row. This only works when the result does not depend on the row.
  • Name the role with to. A policy scoped to authenticated is skipped for anon requests.
  • Move lookups into a security definer function. A policy that joins a membership table re-runs that table's own RLS; a definer function in an unexposed schema avoids it.
  • Filter in the query too. Adding .eq('user_id', userId) on the client gives the planner a condition it can use, even though the policy enforces the same rule.

A strong candidate also says that a policy is a WHERE clause added to every query, so its cost multiplies with the number of rows the query touches.

5. How do you write a team-membership policy without killing performance?

Put the membership check in a security definer function in a schema the Data API does not expose, pin its search_path, and call it from the policy wrapped in a select.

Flip the check so the function returns the set of teams for the current user, which Postgres can evaluate once, instead of checking membership per row.

create schema if not exists private;

create function private.user_team_ids()
returns setof uuid
language sql
security definer
stable
set search_path = ''
as $$
  select team_id
  from public.team_members
  where user_id = (select auth.uid())
$$;

create policy "Team members read projects"
on public.projects for select
to authenticated
using ( team_id in (select private.user_team_ids()) );

A weak answer misses two traps. Creating the function in public makes it callable as an RPC endpoint, and a definer function without set search_path = '' can be hijacked by objects a user creates in another schema.

Interviewers should ask why every name inside is schema-qualified.

6. Find the bug: an admin policy built on JWT metadata.

create policy "Admins can read all orders"
on public.orders for select
to authenticated
using ( (auth.jwt() -> 'user_metadata' ->> 'role') = 'admin' );

Any signed-in user can make themselves an admin. user_metadata comes from raw_user_meta_data, which the user can change with supabase.auth.updateUser({ data: { role: 'admin' } }).

After their next token refresh, the claim is in the JWT and the policy passes. The Supabase docs say not to use raw_user_meta_data for authorization.

The fix is to read app_metadata, which only the server can set, or a roles table looked up through a definer function.

A senior candidate adds that JWT claims are stale until the token refreshes, so revoking admin access takes effect only after the access token expires, and a roles table checked at query time does not have that delay.

A custom access token hook is the third option when the role must be in the token.

7. Find the bug: a post update policy.

create policy "Users update their own posts"
on public.posts for update
to authenticated
using ( (select auth.uid()) = author_id )
with check ( true );

The USING clause limits which rows a user can target, but with check (true) accepts any new row.

A user can run an update that sets author_id to another user's ID and hand their post to someone else, or set it to a value that makes the row disappear from their own view.

On a table with a published or approved flag, the same hole lets users approve their own content.

The fix is with check ( (select auth.uid()) = author_id ).

For columns users should never change, the stronger fix is column privileges: revoke update on public.posts from authenticated, then grant update (title, body) on public.posts to authenticated.

Candidates who reach for column grants have usually been burned by this before.

8. A view returns rows that the underlying table's RLS should hide. Why?

Views run with the privileges of their owner by default, and the owner is usually postgres, which bypasses RLS. A view in public over an RLS-protected table therefore exposes every row through the Data API.

On Postgres 15 and later, create it with (security_invoker = true) so the view checks permissions and policies as the calling role.

create view public.my_open_tasks
with (security_invoker = true)
as
select id, title, due_at
from public.tasks
where done = false;

The same thinking applies to functions: a security definer function in an exposed schema is an RPC endpoint that runs as its owner.

A strong candidate audits views, materialized views (which have no RLS at all) and definer functions when reviewing a schema, not only tables.

Auth and API Keys

9. What are publishable and secret keys, and how do they differ from anon and service_role?

Per the API keys docs, a publishable key (sb_publishable_...) is for public code and resolves to the anon or authenticated role, so RLS applies.

A secret key (sb_secret_...) is for backend code and resolves to service_role, which bypasses RLS. They replace the legacy anon and service_role keys, which Supabase is deprecating by the end of 2026.

The differences a candidate should know:

  • The new keys are short strings, not JWTs. A key starting with eyJ is a legacy key.
  • Each key can be created and revoked on its own. Rotating a legacy key meant rotating the JWT secret, which logged out every user.
  • Secret keys return HTTP 401 when a request comes from a browser, detected by User-Agent. The docs say this does not make a leaked key safe.
  • Both new keys go in the apikey header. Code that sends them as Authorization: Bearer or verifies them as JWTs breaks.

10. Walk through migrating a production project to the new keys.

The migration guide runs in this order: create the new keys (legacy keys keep working), swap the publishable key into browser and mobile code, swap secret keys into servers, update Database Webhooks and pg_net calls to send the key in an apikey header stored in Vault, update Edge Functions, confirm nothing still uses the legacy keys, then deactivate them in the dashboard.

Senior candidates raise the parts that break quietly. Old mobile app versions in users' hands still carry the anon key, so deactivation has to wait for them or ship with a forced upgrade.

Cron jobs and third-party integrations hold copies of service_role. Deactivation is reversible, which makes it a safe test: turn the legacy keys off, watch logs for failures, turn them back on if needed.

11. What are JWT signing keys, and why move off the legacy JWT secret?

Signing keys are the keys Supabase Auth uses to sign user session JWTs. The legacy system used one shared HS256 secret, so anything that verified tokens also held the secret that could mint them.

The new system, announced on July 14, 2025, supports asymmetric keys: ES256 (P-256, recommended) and RS256, with EdDSA listed as coming soon.

Public keys are published at /auth/v1/.well-known/jwks.json, so services can verify tokens without being able to create them.

Per the signing keys docs, keys move through standby, current, previously used and revoked states. A rotation promotes a standby key to current and keeps the old one trusted until you revoke it.

The docs advise waiting at least 1 hour 15 minutes after rotating away from the legacy secret before revoking it when access tokens last 1 hour, so no live session is cut off. Only deleting a key is irreversible.

12. getSession, getUser or getClaims on the server: which one, and why?

Use getClaims for authorization on the server. getSession reads the session from storage, which on the server usually means a cookie the client sent, and it does not verify the token. getUser verifies by calling the Auth server on each request.

Per the getClaims reference, getClaims verifies the JWT against the project's JWKS locally when the project uses an asymmetric signing key, and falls back to contacting the Auth server when it does not.

A strong candidate adds the tradeoff: local verification cannot know that a user was banned or signed out since the token was issued, so a short access token lifetime matters, and an admin action that must take effect at once should check the database or call getUser.

A weak answer trusts getSession().user in middleware, which lets anyone with a forged cookie pass.

13. How do you handle auth in a server-rendered app such as Next.js?

Use @supabase/ssr to create a browser client and a server client that both read and write the session from cookies, refresh the session in middleware so server components receive a valid token, and call getClaims on the server before trusting the user.

The PKCE flow sends the auth code to a callback route that exchanges it for a session with exchangeCodeForSession.

Follow-ups that separate levels:

  • Server components cannot set cookies, so the refresh has to happen in middleware or a route handler.
  • Cookies are limited to about 4,096 bytes each. Large custom claims in the JWT push the session over that limit, and the library splits it across chunked cookies.
  • Pages that render user data must not be cached by a CDN as if they were public.

Our Next.js coding challenges cover the framework side.

14. How would you add a custom role claim to Supabase access tokens?

Write a custom access token hook: a Postgres function (or HTTP endpoint) that Supabase Auth calls before issuing a token, which receives the claims and returns them with additions.

A typical hook reads the user's role from a user_roles table and sets a user_role claim.

The function has to be granted to supabase_auth_admin and revoked from authenticated, anon and public, and the roles table needs a policy that lets supabase_auth_admin read it.

Candidates should then explain how RLS reads it, (select auth.jwt() ->> 'user_role'), and why the claim is only as fresh as the token (question 6).

A strong answer keeps the claim small, since it lands in every cookie, and keeps the source of truth in the table.

Storage and Realtime

15. How does Storage access control work?

Storage keeps file metadata in the storage.objects table, and access is controlled with RLS policies on that table.

A public bucket serves files to anyone with the URL for downloads, but uploads, updates and deletes still go through policies. A private bucket requires a policy or a signed URL for every read.

create policy "Users upload to their own folder"
on storage.objects for insert
to authenticated
with check (
  bucket_id = 'avatars'
  and (storage.foldername(name))[1] = (select auth.uid())::text
);

Details a senior candidate brings up: upserting a file needs select and update policies as well as insert, signed URLs are bearer links that work for anyone until they expire, and a policy based on a folder name only works if the upload path is built on the server or checked by the policy, never trusted from the client.

16. When do you serve files through a signed URL instead of a public bucket?

Use a public bucket for content that would be fine on a public CDN, such as product images and avatars, because public URLs cache well.

Use a private bucket with short-lived signed URLs for anything tied to a user or a contract: invoices, uploaded documents, exports.

For uploads from the browser, a signed upload URL lets the server decide the path and lets the client upload directly without proxying the file.

The tradeoff candidates should state is that a signed URL cannot be revoked before it expires, so expiry should match how long the link is needed.

A common mistake is generating signed URLs with a secret key in a server action for any path the client asks for, which turns the server into an open door around the bucket's policies.

17. Compare Postgres Changes, Broadcast and Presence.

  • Postgres Changes streams inserts, updates and deletes from tables in the supabase_realtime publication. Each change is checked against the subscriber's RLS policies, which costs a read per subscriber per change and limits throughput on busy tables.
  • Broadcast sends low-latency messages between clients on a channel. It can also be sent from the database with realtime.send or a trigger using realtime.broadcast_changes, which scales better than Postgres Changes for many subscribers.
  • Presence tracks and syncs shared state such as who is online in a room.

A strong candidate picks Broadcast from the database for a chat or feed with many listeners, keeps Postgres Changes for low-volume admin views, and knows that Realtime Broadcast gained binary payload support in June 2026.

18. How do you stop users from joining a Realtime channel they should not see?

Use private channels and write RLS policies on realtime.messages.

Per the Realtime Authorization docs, select policies control joining and receiving, insert policies control sending, realtime.topic() returns the channel being joined, and the extension column distinguishes broadcast from presence.

Turning off "Allow public access" in Realtime Settings forces every channel to be private.

create policy "Room members receive broadcasts"
on realtime.messages for select
to authenticated
using (
  extension = 'broadcast'
  and exists (
    select 1 from public.room_members m
    where m.user_id = (select auth.uid())
      and m.room_topic = (select realtime.topic())
  )
);
const channel = supabase.channel('room-42', { config: { private: true } })

The catch: permissions are cached for the connection and refresh only when the client reconnects or sends a new JWT. A user removed from a room keeps receiving messages until their token expires.

The docs also warn against putting alter table realtime.messages enable row level security in a migration; it is already enabled, and the statement fails with an ownership error.

Edge Functions and Connections

19. What are Edge Functions good for, and what are the limits?

Edge Functions are TypeScript handlers running on the Supabase Edge Runtime, a Deno-compatible runtime; all regions have run a Deno 2.1 compatible release since August 15, 2025.

They suit webhooks, calls to third-party APIs with secrets, and short server logic next to the database. The limits page sets the boundaries:

  • 256MB memory and 2 seconds of CPU time per request.
  • Wall clock of 150 seconds on Free and 400 seconds on paid plans; a request idle for 150 seconds returns 504.
  • Bundle size of 20MB when built by the CLI, 5MB when bundled server-side.
  • 100 functions per project on Free, 1,000 on Pro, 2,000 on Team.

A candidate who suggests an Edge Function for video transcoding or a multi-minute ETL job has not read these numbers. The right answer is a queue (Supabase Queues runs on pgmq) and a worker outside the request path.

20. Find the bug: an Edge Function that returns a user's orders.

import { createClient } from 'npm:@supabase/supabase-js@2'

const admin = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
)

Deno.serve(async (req) => {
  const { userId } = await req.json()
  const { data } = await admin.from('orders').select('*').eq('user_id', userId)
  return Response.json(data)
})

Any caller can read any user's orders. The function queries with the service role, which bypasses RLS, and trusts a userId from the request body. With verify_jwt on, the caller needs a valid JWT, but nothing ties that JWT to userId.

The fix is to query as the caller. The Edge Functions auth guide now recommends the @supabase/server package, whose withSupabase wrapper verifies the user and hands the handler a client scoped to the caller's RLS:

import { withSupabase } from 'npm:@supabase/server@1'

export default {
  fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => {
    const { data, error } = await ctx.supabase.from('orders').select('*')
    if (error) return Response.json({ message: error.message }, { status: 500 })
    return Response.json(data)
  }),
}

A strong candidate keeps ctx.supabaseAdmin for the narrow writes that need it and lets RLS filter the read. Note that the framework adapters in @supabase/server (Hono, H3, Elysia, NestJS) are deprecated and removed on December 1, 2026.

21. How should a serverless backend connect to a Supabase database?

Through the shared Supavisor pooler in transaction mode on port 6543, per the connection guide.

The direct connection on db.[ref].supabase.co:5432 is IPv6 unless you buy the IPv4 add-on, and session mode holds a server connection per client, which runs out fast with many short-lived functions.

Transaction mode does not support prepared statements on the shared pooler, so the driver must turn them off: prepare: false for Postgres.js and Drizzle, pgbouncer=true for Prisma, statement_cache_size=0 for asyncpg.

The guide also recommends creating the client once at module scope with a pool size of 1. A candidate who sees intermittent "prepared statement already exists" errors in production should name this cause at once.

Migrations and pg_dump belong on the direct or session connection, because they rely on session state.

22. When do you write logic as a Postgres function instead of an Edge Function?

Use a Postgres function (called with supabase.rpc()) when the work is mostly database reads and writes that must happen in one transaction, such as transferring credits between two rows or creating an order with its line items.

It runs next to the data, commits or rolls back as a unit, and respects RLS when it is security invoker, the default.

Use an Edge Function when the logic calls external services, needs npm packages, handles webhooks with signature checks, or should not hold a database transaction open while waiting on the network.

Interviewers can ask for the failure case: an Edge Function that makes three separate Data API calls to update related rows leaves the data half-written when the second call fails.

CLI, Migrations and Branching

23. Describe a sound migration workflow for a Supabase project.

Develop against the local stack (supabase start), write schema changes as SQL files in supabase/migrations, test them with supabase db reset, which replays every migration and the seed file, and apply them to hosted projects only from CI.

Changes made in the dashboard on production get pulled back with supabase db pull --remote so the repository stays the source of truth.

A strong candidate also mentions supabase gen types to regenerate TypeScript types after each migration, pgTAP tests run with supabase test db for RLS policies, and the rule that a migration already applied to production is never edited, only followed by a new one.

CLI v3 also made pg-delta the default engine for db diff, replacing migra unless you turn it back on in config.toml.

24. Find the bug: a deploy job after upgrading to CLI v3.

# .github/workflows/deploy.yml (excerpt)
- uses: supabase/setup-cli@v3
  with:
    version: latest
- run: supabase link --project-ref "$PROJECT_REF"
- run: supabase db push
- run: echo y | supabase functions deploy

Under v2 this pushed migrations to the linked project. Under CLI v3, a bare db push targets the local stack, so the step does nothing to production (or fails with no local stack).

Every command that changes a hosted project now asks for confirmation and fails in unattended runs without --yes, and a y piped on stdin no longer confirms.

- uses: supabase/setup-cli@v3
  with:
    version: 3.0.0
- run: supabase link --project-ref "$PROJECT_REF"
- run: supabase db push --yes --remote
- run: supabase functions deploy --yes

Pinning the version is part of the fix: latest moved this pipeline to a major release without review.

A senior candidate also checks for stray SUPABASE_* variables, since v3 makes flags and environment variables override config.toml, and renames any old [inbucket] section to [local_smtp], which v3 requires.

25. How does Supabase branching work, and what does a branch contain?

A branch is a separate Supabase instance with its own database, Auth, Storage and API keys.

Per the branching docs, preview branches are created per pull request with the GitHub integration and deleted when the PR is merged or closed; persistent branches are long-lived for staging or QA.

Branches created from the dashboard (in beta) build their schema from the base project's migration history.

The common misconception: a branch does not clone production data. It starts empty and builds its schema from migrations, with data coming from a seed file or the dashboard's "Include data" option.

Merging runs a deployment workflow (clone, pull, health, configure, migrate, seed, deploy) against production. A strong candidate treats a failing migration on the preview branch as the signal that production would fail too.

26. How do you seed and test RLS policies so regressions get caught?

Seed realistic users and rows in supabase/seed.sql, then write pgTAP tests that switch role and claims and assert what each user can see and change. Run them locally and in CI with supabase test db.

begin;
select plan(2);

set local role authenticated;
set local request.jwt.claims = '{"sub":"11111111-1111-1111-1111-111111111111","role":"authenticated"}';

select is(
  (select count(*) from public.notes)::int,
  2,
  'user 1 sees only their own two notes'
);

select throws_ok(
  $$ insert into public.notes (user_id, body)
     values ('22222222-2222-2222-2222-222222222222', 'spoof') $$,
  '42501',
  null,
  'user 1 cannot insert as user 2'
);

select * from finish();
rollback;

A candidate who tests policies only through the app UI will miss the cases attackers try first: writing another user's ID, updating a row into someone else's ownership, and calling RPC functions directly.

pgvector and AI Workloads

27. How do you store embeddings and run similarity search in Supabase?

Enable the vector extension, add a vector(n) column sized to the embedding model, build an HNSW index with the operator class that matches your distance function, and expose the search as a Postgres function so the client can call it through RPC with RLS applied.

create extension if not exists vector with schema extensions;

create table public.documents (
  id bigint generated always as identity primary key,
  owner_id uuid not null default auth.uid(),
  content text not null,
  embedding extensions.vector(1536)
);

create index on public.documents
using hnsw (embedding extensions.vector_cosine_ops);

create function public.match_documents(query_embedding extensions.vector(1536), match_count int)
returns table (id bigint, content text, similarity float)
language sql stable
set search_path = ''
as $$
  select d.id, d.content, 1 - (d.embedding operator(extensions.<=>) query_embedding) as similarity
  from public.documents d
  order by d.embedding operator(extensions.<=>) query_embedding
  limit match_count
$$;

The HNSW guide recommends HNSW as the default index and notes that it can be built on an empty table, unlike IVFFlat, which needs data to build its lists.

Indexes support up to 2,000 dimensions for vector and 4,000 for halfvec, so a 3,072-dimension model needs a halfvec cast to be indexed.

28. A filtered vector search returns 3 results when you asked for 10. Why?

With an approximate index, pgvector applies the WHERE filter after the index scan. The pgvector README gives the arithmetic: HNSW's default hnsw.ef_search is 40 candidates, so a filter that matches 10% of rows leaves about 4 results.

RLS policies are filters too, so a multi-tenant table where each tenant owns a small slice hits this on every query.

Fixes a strong candidate lists: enable iterative index scans (pgvector 0.8.0 and later) with set hnsw.iterative_scan = relaxed_order, raise hnsw.ef_search per query, use a partial index or partitioning per large tenant, or filter first with a B-tree index when the filtered set is small enough for an exact scan.

A weak answer blames the embedding model.

What Changed Recently

Jul 14, 2025
JWT signing keys and publishable and secret API keys announced
Aug 15, 2025
Edge Functions run a Deno 2.1 compatible release in all regions
Dec 11, 2025
Data API upgraded to PostgREST v14
Apr 16, 2026
pg-delta declarative schema management in public alpha
May 28, 2026
Passkeys for Supabase Auth in beta
Jul 1, 2026
Postgres 14 support ends on the platform
Oct 30, 2026
New tables no longer exposed to the Data API without a GRANT, on all projects
Nov 2, 2026
Supabase CLI v3.0.0: local by default, --remote, confirmation for hosted changes

29. What changed about table exposure in 2026, and what breaks?

Per the changelog, the default grants that gave anon, authenticated and service_role select, insert, update and delete on every new public table are gone.

New projects got the new default from May 30, 2026, and all existing projects from October 30, 2026. Existing tables keep their grants, and direct Postgres connections are unaffected.

grant select on public.your_table to anon;
grant select, insert, update, delete on public.your_table to authenticated;
grant select, insert, update, delete on public.your_table to service_role;

alter table public.your_table enable row level security;

The breakage: a migration written last year that creates a table and policies but no grants now produces a table the app cannot reach, and PostgREST returns a 42501 permission denied error with a hint naming the missing grant.

A strong candidate sees this as a security improvement, because a table now needs two deliberate steps, a grant and a policy, before the browser can touch it.

30. What did Supabase CLI v3 change for local development and CI?

CLI v3.0.0 shipped on November 2, 2026, after v2.122.0 (October 23, 2026). The main changes:

  • Local by default. db push, db pull, db dump, migration list, gen types and the storage commands target the local stack unless you pass --remote, --remote=<ref>, --remote=<branch> or --remote=<postgres-url>. --linked, --project-ref and --db-url still work with a warning and go away in v4.
  • Consent for hosted changes. Prompts default to No, unattended runs fail with HostedMutationConsentRequiredError unless given --yes.
  • Precedence. Flags beat shell environment, which beats project env files, which beat config.toml.
  • Removals. db remote commit becomes db pull --remote, gen keys becomes projects api-keys, and an [inbucket] config section fails to load.

The release also introduced an experimental local stack where each project or worktree gets its own instance, including supabase start --runtime native with no container engine.

Candidates who run parallel agents or worktrees should know it exists and that its flags may still change.

31. Which Postgres versions does the platform run, and how does an upgrade work?

Postgres 17 is the platform default, and support for Postgres 14 ended on July 1, 2026, with remaining projects upgraded automatically.

The Postgres 17 image dropped timescaledb, plv8, plls, plcoffee and pgjwt, so projects using them need a plan before upgrading.

Per the upgrade docs, the dashboard upgrade copies data to a new instance and runs pg_upgrade, takes the project offline for a window that grows with database size, and requires deleting read replicas first.

A strong candidate also tracks minor releases: the September 25, 2026 15.19 and 17.11 release warned that some ltree, btree_gist and custom operator indexes may need a reindex.

Since August 5, 2026, explicit versions in create extension statements are ignored in favor of platform defaults, which matters for migrations that pinned one. Our PostgreSQL interview guide covers the database side in more depth.

32. Supabase or Firebase for a new product, and what would make you switch?

Pick Supabase when the data is relational, the team knows SQL, and you want to keep the option to leave: it is Postgres, so pg_dump moves the data anywhere, and the open-source stack can be self-hosted.

Pick Firebase when the app is mobile-first with offline sync as a core requirement and the data fits documents, or when the team is already deep in Google Cloud.

The design-tradeoff answer interviewers want covers costs that show up later: Supabase pricing follows compute size, disk and egress, so a missing index costs money as well as speed, while Firestore bills per document read, so a chatty listener multiplies the bill.

Security models differ too: RLS in SQL against Firestore security rules.

A candidate who has migrated in either direction can describe moving auth users and their password hashes, which is the step that usually decides whether the switch is painless. See our Firebase interview guide for the other side.

Signs of a Strong Answer

  • They describe access as two layers, grants and RLS policies, and check both when a table is created.
  • They write (select auth.uid()), name the role with to, and index the columns a policy filters on without being prompted.
  • They put authorization data in app_metadata or a table, never in user_metadata, and know JWT claims are stale until refresh.
  • They keep secret keys on the server, query as the user wherever RLS can do the filtering, and treat service role code as code that needs its own checks.
  • They connect serverless code through the transaction pooler with prepared statements off, and run migrations on a direct or session connection.
  • They pin the CLI version in CI and know what --remote and --yes do in v3.

Hiring Supabase Developers

Engineers who have shipped a Supabase app to real users, written RLS that survived a security review and moved a project through a CLI or key migration are hard to find from a resume, because most Supabase experience on paper comes from side projects.

Second Talent helps US startups hire vetted AI-Native Engineers from Asia, including full-stack developers and backend developers who build on Postgres, screened with questions like these.

For related stacks, see our PostgreSQL and full-stack developer interview guides.

Rates run $23 to $39 USD an hour, billed monthly, with matches in 24 hours and a 90-day one-time replacement guarantee. Compare budgets on the cost to hire a full-stack developer page, or tell us what you need.

Get developers who ship with AI agents.

Starts at $23/Hour.

Start Hiring

$0 cost until you hire