Understand, AI agent sometimes answer questions a kind like in parallel world though. Is this issue related to manual login to supabase dashboard, or is it a login simulation from code programatically for testing or whatever purpose? I hit similar issues in other products before due to Chrome extensions, brower cache ... If you are talking about login problem via https://supabase.com/dashboard/org/slug for below dashboard: <img width="1569" height="496" alt="image" src="https://github.com/user-attachments/assets/52a40827-deff-4db5-b956-905bfaf8261e" /> If it's the manual login problem, may I trouble you to add more details in the question like: OS, browser, the cloud supabase or self-hosted supabase, supabase plan, and things you've tried (such as Incognito, other browsers ...) etc.
It's from Claude Code. Does it help anyway, or just none-sense?
One thing worth pulling out of the OP's own evidence before leaning on the RLS angle: the **hand-inlined SELECT returns 3**. If Postgres is auto-inlining the SQL function (the usual suspect), that inlined plan should behave like the manual inline — so "inlining flips the RLS check" doesn't cleanly explain standalone-function = 0 vs manual-inline = 3 for a `SECURITY INVOKER` function running as the same role in the same transaction. RLS is evaluated as the invoker either way. That points away from RLS and toward what's resolved *inside* the function boundary — operator/opclass under `search_path`, or the vector parameter's cast. So I'd test in this order: 1. `alter function public.match_kb_chunks(vector, integer) set search_path = public, extensions;` — cheapest; if it flips to 3 rows, done. 2. If still 0: `EXPLAIN (VERBOSE, ANALYZE)` the function call vs the manual inline and diff them — check which `<=>` operator/opclass each resolves and whether one uses the hnsw/ivfflat index while the other does a seq scan. Operator drift or a cast difference will show up right there. And a caution on `SECURITY DEFINER`: if it "fixes" it, that's really just masking RLS (the function runs as owner and skips the policy), not explaining the 0 — only worth it if you actually intend the match to bypass RLS.
That's really useful — and it changes the picture. **One** callback hit means we can drop the double-exchange / Cloudflare-prefetch theory; Google's code isn't being consumed twice. So it's a single, clean exchange that Google itself is rejecting: `unexpected_failure: Unable to exchange external code: 4/0A…` (the `4/0A…` is just Google's normal auth-code prefix, not a truncated code). You reach the consent screen, so the redirect URI is registered fine — the failure is purely GoTrue → Google at the token step. With the redirect URI confirmed and only one exchange, the usual culprit for that exact error is the **client secret**: - Regenerate it in Google Cloud → APIs & Services → Credentials, and paste the fresh value into Supabase → Authentication → Providers → Google (then remove the old secret in Google). A wrong secret makes Google return `invalid_client`, which GoTrue surfaces as "unable to exchange external code" — and "looks correct" often hides a rotated/expired one. - I also spotted `authuser=2`, so you're using a secondary Google account. If your OAuth consent screen is still in **Testing**, only listed test users can finish the flow — add that account as a test user (or publish the app). Two quick things: is the consent screen in *Testing* or *In production*, and when you say the secret's verified — was it freshly regenerated and re-pasted, or just visually matched? I'd start with a brand-new secret; that's the most common fix once the redirect URI checks out.
Thanks for sharing the implementation, @hieuwu — and honestly, real credit to you here. Proposing a forward-looking feature *and* building a working Android + Edge Function reference for it is exactly what turns a "someday maybe" request into something the team and everyone hitting the same need can actually evaluate. The iOS write-up will be a great addition whenever it's ready — I'll keep an eye out for the update. Appreciate the effort you've put into this. 🙌
Thanks for sharing the implementation, @hieuwu — and honestly, real credit to you here. Proposing a forward-looking feature *and* building a working Android + Edge Function reference for it is exactly what turns a "someday maybe" request into something the team and everyone hitting the same need can actually evaluate. The iOS write-up will be a great addition whenever it's ready — I'll keep an eye out for the update. Appreciate the effort you've put into this. 🙌
You've isolated it correctly: correct tenant username, `sslmode=require`, session mode on 5432, and it still stalls after TCP while Supavisor's internal Postgres auth keeps succeeding in the logs — that combination really does point at the pooler/LB listener for your project, not your client. And the `db.<ref>.supabase.co` "no address" is expected, not a second bug: direct connections are IPv6-only now unless you add the IPv4 add-on, so on free tier the pooler *is* your intended path — which is exactly why it being broken is worth escalating rather than working around. I should be upfront: I'm another community member, not Supabase staff, so I can't touch your project's infra — the support ticket is genuinely the only route to the actual fix here. That said, two things will make the ticket move faster, and one will get you unblocked today. **Sharpen the ticket (2 min each):** 1. Prove it's the transport, not Prisma — try plain `psql` on the same string: ``` psql "postgresql://postgres.qykgmpwyztxvyyfzdgds:REDACTED@aws-1-eu-central-1.pooler.supabase.com:5432/postgres?sslmode=require" ``` If `psql` hangs identically, that rules out Prisma entirely — attach it to the ticket. 2. Test the **transaction pooler on 6543** (same host, port 6543). If 6543 connects but 5432 hangs, that pins it to the *session-mode* listener specifically — a very useful signal for support, and it means your app's normal (non-migration) traffic could run via 6543 in the meantime. **Unblock migrations now, without the pooler:** since the Management API / SQL editor works, generate the migration SQL locally and apply it there. No database connection needed: ``` npx prisma migrate diff \ --from-migrations ./prisma/migrations \ --to-schema-datamodel ./prisma/schema.prisma \ --script > pending.sql ``` (Use `--from-empty` instead of `--from-migrations ...` if the DB has never had migrations applied.) Paste `pending.sql` into the Supabase **SQL Editor** and run it. Then, if you're on the `migrate` workflow, tell Prisma it's applied so state stays in sync: ``` npx prisma migrate resolve --applied <the_migration_name> ``` If you're using `db push` rather than `migrate`, just running the diffed SQL in the editor is enough. That gets your week of blocked schema changes in today, independent of whatever's wrong with the pooler. I know a week of being blocked is rough — the SQL-editor route should get you moving right now, and the `psql`/6543 results are the fastest way to get support to the actual root cause. Share those two results here and I'll help you read them.
"No backups found" when trying to resume is a server-side problem — the resume flow needs a snapshot to bring the project back, and it's reporting that it can't locate one, which isn't something you can resolve from the dashboard no matter how many times you retry. So the browser/incognito/different-browser attempts won't help here; this needs Supabase to look at the instance. What I'd do: - Open a support request with the **project ref** (`dbsdrrcjqibvxosyuhhj`) and the exact error, and mark it as blocking — the resume/restore machinery is entirely on their side. - Note your plan: on **free tier**, backup retention is limited, so if the project sat paused long enough there's a real chance the backing snapshot has aged out — which would be why the resume can't find one. That's worth raising directly so they can tell you whether the data is recoverable. I don't want to give false hope: if there genuinely is no backup available for a free-tier project, there may not be a way to bring the old data back, so the sooner you escalate to support the better. If you have any recent `pg_dump`/export of your own, hold onto it — and once you're unblocked (either resumed or on a fresh project), consider a paid plan with PITR for anything you can't afford to lose.
Region availability isn't something anyone in the community can enable — it depends on Supabase supporting the underlying AWS region — so the useful thing here is to keep this as a Feature Request and gather votes; demand signal is genuinely what drives new regions onto the roadmap. A couple of practical notes while a Türkiye region isn't available: - If the driver is **data residency**, that's the real blocker and only an official region solves it — worth stating that explicitly in the request, since residency requirements weigh more heavily than latency. - If the driver is mainly **latency** for users in Türkiye, the closest existing regions (e.g. eu-central / Frankfurt) are usually a reasonable interim, and putting a CDN in front of your frontend + caching read-heavy responses takes the edge off the round-trip to the DB region. So: no way to self-serve it, but add your use case (residency vs. latency) to the request — that context is exactly what helps the team prioritize.
This comes up a lot, and the "everything in one platform, one bill" appeal is real — especially for the AI-assisted, build-fast workflow you describe. For now Supabase is deliberately the backend half (Postgres, Auth, Storage, Edge Functions, Realtime), and the expected pattern is to pair it with a dedicated frontend host — Vercel, Netlify, or Cloudflare Pages/Workers for SPAs and SSR frameworks. In practice that pairing is pretty smooth (the framework guides cover the env-var wiring), so the gap today is more about consolidation of billing/ops than capability. Static/SPA builds (React, Vite, plain SPA) will deploy to any of those hosts with basically zero backend-specific config; Next.js SSR is the one where the host choice matters more (Vercel or Cloudflare for the server runtime). As a product direction it's a legitimate thing to want, so keep it as a Feature Request and let it accumulate upvotes — "first-party frontend hosting" is a recurring ask and the vote signal is what helps it get prioritized. I don't think there's a native option coming that I can point you to today, so the honest answer is: pair with a frontend host for now.
Natural-language-to-SQL is a neat idea, and framing it as an optional developer-experience layer on top of the existing stack (rather than a change to core query execution) is the right way to think about it — the security model shouldn't move. Worth knowing there's already a chunk of this today: the **Supabase AI Assistant** in the dashboard does NL→SQL interactively (describe what you want, it drafts the query, you review and run). What doesn't exist is a *programmatic* endpoint your app could call at runtime, which is what you're asking for. The reason a runtime NL→SQL API is a bigger ask than it looks is the safety surface: turning arbitrary user text into executed SQL is a prompt-injection / privilege-escalation risk unless every generated query is constrained (read-only role, statement timeouts, allow-listed tables, and RLS still doing the real authorization). So if you want something like this now, the buildable version is: an Edge Function that sends the NL request + your schema to an LLM, gets SQL back, runs it through a **restricted read-only role** with RLS enforced and a hard timeout, and returns the rows — never executing model output with elevated privileges. That gets you the feature with the guardrails you'd want a native version to have anyway. As a first-party feature it's reasonable to request; I'd keep it explicitly scoped as "optional, opt-in, RLS-and-permission-preserving" like you have, and let it gather upvotes.
This is a genuinely useful quality-of-life request — inspecting `timestamptz` data in UTC when your team lives in KST is a constant little friction, and since you're only asking to change the *display*, not the stored value, it's a safe scope. For now the Table Editor always renders `timestamptz` in UTC with no display-timezone option, so the practical workaround is to inspect via the SQL Editor (or a view) with an explicit conversion: ```sql select id, created_at, created_at at time zone 'Asia/Seoul' as created_at_kst from your_table order by created_at desc; ``` `at time zone 'Asia/Seoul'` gives you the local wall-clock value for reading, while the stored column stays UTC. If your team inspects the same tables a lot, wrapping that in a view (`..._local`) makes it a one-click thing in the Table Editor. That's clearly clunkier than a display preference, though. A dashboard/project-level "display timestamps in: UTC / browser local / custom TZ" toggle is a clean, low-risk feature (display-only, no data change), and "just use the browser's local timezone" alone would cover most of the pain. Worth keeping as a Feature Request and upvoting — it's the kind of small Studio improvement that helps basically everyone outside UTC.
Interesting one — SD-JWT / Digital Credentials-based email verification is pretty forward-looking, and you're right that it doesn't map onto the existing OIDC-based auth paths, so there's no native hook for it today. The Edge Function approach you've built (receive the SD-JWT, validate issuer + signature against Google, then flip the verification status) is genuinely the correct pattern for now — it's how you'd integrate any auth mechanism GoTrue doesn't natively understand. The rough edge is exactly what you called out: you have to reach into the user's verification state yourself, which means using the Admin API (`auth.admin.updateUserById` with `email_confirm: true`) from the function with the service role, plus your own nonce/replay protection. Doable, but not the "drop-in" experience a first-class API would give. For it to become native, GoTrue would need a verification endpoint that accepts an SD-JWT presentation and does the issuer/signature/selective-disclosure validation server-side — that's a real feature, not a config toggle, so this is right to raise as a Feature Request. I'd keep the writeup (the sequence diagram especially) attached, since it gives the team a concrete flow to react to, and let it gather upvotes. If you want, sharing your working supabase-kt + Edge Function solution in the thread would also help others who need this before any native support lands.
Glad you're enjoying it! pgPointcloud would be a nice addition for LiDAR work, but it isn't in the managed extension list today, and you can't add arbitrary extensions yourself on hosted Supabase (only the ones Supabase has vetted and packaged), so this is genuinely a feature request for the team rather than something you can enable. Worth keeping the request open and getting other geospatial folks to upvote — pgPointcloud pairs naturally with PostGIS (which *is* available), so there's a coherent story for it. In the meantime, depending on what you're doing with the point clouds, a couple of interim options: - If you mainly need to **store and query** points spatially, PostGIS `geometry(PointZ)` / `MultiPointZ` covers a lot of ground (indexing, spatial filters, nearest-neighbour) even if it's not as compact as PcPatch. - If you need the heavier LiDAR processing (patch compression, PDAL pipelines), do that **outside** the DB and land the results — derived features, tiles, or PostGIS geometries — into Supabase. Not as clean as native pgPointcloud, but it keeps you moving while the extension request is under consideration.
This is a fair request, and the mobile reasoning is sound — a 6-digit code really is the better UX than a deep-linked confirmation URL, which is fragile across email clients and OS versions. The constraint you've hit is the 3 June 2026 change: on the free tier with the built-in email service, the templates are locked to their defaults, and the password-reset default is the `{{ .ConfirmationURL }}` link. Today the **only** way to switch that flow to `{{ .Token }}` is to set up **custom SMTP** (Authentication → SMTP Settings), which as you say needs a verified sending domain. So there's a real workaround, just not a free one. Your framing of the ask is the smart way to pitch it, too — not "reopen free-text template editing", but "allow this one variable for this one flow", e.g. a locked toggle between link and token on the reset template, or a second locked default variant. That's a much smaller surface for the team to consider than reopening full customization, and it keeps the anti-abuse reasoning behind the June change intact. Worth leaving this as a Feature Request and getting other mobile devs to upvote — the OTP-vs-link toggle for recovery/magic-link on free tier is a common pain point, and a scoped, no-free-text version is the kind of thing that has a real chance of landing.
This is a well-researched writeup — and the root cause you've traced is accurate: auth-js evaluates expiry against `Date.now()` in `__loadSession`, `_autoRefreshTokenTick`, and `expiresAt()`, so a device clock running fast enough makes every fresh token look pre-expired, and you get the refresh storm → 429 → wedged session. The 288 refreshes/36 min repro is a pretty damning illustration. A server-time-offset approach (derive skew from a server-provided timestamp — e.g. the `Date` response header or the token's `iat` vs local clock at receipt — and apply it to the expiry math) is the right shape for a fix, since it makes the client self-correcting without weakening security. The awkward part today is there's no clean injection point: `EXPIRY_MARGIN_MS` isn't configurable, and the expiry checks are internal, so you can't work around it purely from userland without patching auth-js. The best interim mitigation I've found is **detection rather than correction** — watch for the signature (rapid successive refreshes / a 429 from the token endpoint) and surface an explicit "your device clock looks off, please enable automatic date & time" message, so the user isn't staring at a generic "can't log in". Not a fix, but it turns an invisible failure into an actionable one. Definitely worth filing on the auth-js repo (supabase/auth-js) in addition to here, given you've already localized it to specific functions and versions — that's most of the work a maintainer would need to scope a `getServerTimeOffset`-style change. I'd upvote it.
Agree this is a real gap — fixed-format drains are hard to slot into anything that expects its own shape, and the "add a session-id header and correlate manually in Sentry" trick is exactly the kind of workaround that shows the feature needs a transform step. Until there's native customization, the pragmatic pattern is to **drain to something you control and reshape there**: point the log drain at your own HTTP endpoint (an edge function, a small worker, or a Vector/Fluent Bit instance), enrich/rename fields — inject your trace id, remap to Sentry's expected structure — and forward on. Vector in particular is well-suited to this (an `http_server` source → `remap` transform with VRL → `sentry`/`http` sink) and gets you the trace-id association you're after without waiting on the feature. That's clearly more moving parts than a built-in transform, so it's a reasonable thing to want first-class. Worth keeping this as a Feature Request and getting others who drain to Sentry/Datadog/etc. to upvote — the "let me set a trace id / template the payload before it leaves" ask is broadly useful, not just your case.
You can resend it — the confirmation email just has a short expiry, it's not a dead end. Programmatically, use `resend`: ```js const { error } = await supabase.auth.resend({ type: 'signup', email: 'you@example.com', }) ``` Or from the dashboard: **Authentication → Users**, find the user, and use the row's menu to resend the confirmation (or confirm them manually). Two things that trip people up if the resend also seems to do nothing: - The **default email service is rate-limited** (only a few messages/hour) and is really meant for testing — if you've requested a bunch of these in a row you may be throttled. For anything real, set up custom SMTP under **Authentication → SMTP Settings**. - Check spam, and make sure the confirmation link's redirect URL is in **Authentication → URL Configuration** so the click actually lands back in your app.
Short version: on hosted Supabase you can't relocate `pg_net`, and this particular advisor finding is a known accepted exception — not something you're expected to fix. `extrelocatable = false` means the extension declares that its objects can't be moved with `ALTER EXTENSION ... SET SCHEMA`, so there's no supported path to shift it out of `public` yourself, and forcing it (editing catalogs / dropping-recreating in another schema) is exactly the kind of unsupported surgery you're right to avoid on a managed instance. So the recommended posture for audit purposes is: - Treat `extension_in_public` for `pg_net` as an **accepted/acknowledged exception**. It's a platform-managed extension living where Supabase puts it; the advisory is flagging placement, not an actual privilege escalation you can act on. Document it as "managed extension, non-relocatable, placement controlled by Supabase" in your remediation evidence. - Don't grant anything extra on it — `pg_net`'s functions aren't exposed to `anon`/`authenticated` by default, so the practical exposure is nil. - If a clean advisor report is a hard requirement, the right move is a feature request / support ask for Supabase to relocate `pg_net` to the `extensions` schema on managed instances (they've been migrating other extensions out of `public` over time; `pg_net` is one of the stragglers). That's genuinely the correct answer here rather than a workaround — the one remaining advisory item being a non-relocatable managed extension is a defensible "accepted risk" line in a hardening review.
This looks like the same root cause as the other post-JWT-signing-key reports: PostgREST can't verify the token's signature, so it silently downgrades the request to the `anon` role — and once you're `anon`, `auth.uid()` is NULL and your `with check (wlasciciel_id = auth.uid())` policy fails with 42501. The reason it works in the SQL editor is that you're setting `request.jwt.claims` by hand there, so you're bypassing the verification step entirely and only testing the policy logic (which is correct). The thing to check is whether the access token your app sends is actually verifiable by *this* project's current keys: 1. Decode the token (jwt.io) and compare its header `kid` against `https://<ref>.supabase.co/auth/v1/.well-known/jwks.json`. If the `kid` isn't in that JWKS, PostgREST won't trust it → anon → NULL `auth.uid()`. 2. Confirm the token is a **freshly minted** one (sign out/in) so it's signed by the currently-active signing key, not a cached older token. 3. Confirm the request really carries `Authorization: Bearer <user access_token>` and that it's distinct from the `apikey` header (sounds like you've verified this). What makes your report notable is that it reproduces on a **brand-new project** with fresh tokens. If a just-issued token's `kid` genuinely isn't present in the project's published JWKS, that's not something you can fix from the app side — it's a platform issue with the new asymmetric signing keys, and worth a support ticket referencing both project refs, with a redacted token header + the JWKS response so they can see the `kid` mismatch directly. If, on the other hand, the `kid` *does* match, then the next place to look is whether your client is attaching the token to the PostgREST call at all (a global `supabase` client created before the session is set can end up sending only the anon key).
Your instinct about the JWT signing key rotation is very likely right, and the "works in the SQL editor with claims set manually, fails from the live app" split is the tell. Here's the mechanism: `auth.getUser()` succeeding only proves the Auth service can validate the token — Auth uses its own verification path. PostgREST verifies the JWT **independently**, and when it can't verify a token's signature it doesn't error loudly — it silently falls back to the `anon` role with no claims. So `auth.uid()` is NULL, `request.jwt.claims` is empty, and even a `with check (true)` policy for `authenticated` rejects the request, because the request isn't running as `authenticated` at all. That matches everything you're seeing. After a rotation from HS256 → ECC (P-256), the usual causes are: 1. **Stale tokens.** Any session minted before the rotation was signed by the old key. If the old key was moved out of the verification set (or the client is still replaying an old access token), PostgREST can't verify it. Force a fresh token: sign the user out/in, or trigger a refresh, so the access token is signed by the now-active key. 2. **`kid` not in the JWKS.** Decode the failing access token (jwt.io) and check its header `kid` against `https://<ref>.supabase.co/auth/v1/.well-known/jwks.json`. If the token's `kid` isn't in that set, that's your smoking gun — PostgREST won't trust it. 3. **Verification key set during migration.** In Settings → JWT Keys, the newly rotated key should be the **current** signing key, and the previous key should remain in the **standby/verification** set until all old sessions expire. If the legacy key was fully removed too soon, in-flight sessions break. Start with #2 — the `kid`-vs-JWKS check tells you immediately whether it's a stale token (re-login fixes it) or the project's key set is genuinely inconsistent. If freshly-minted tokens *still* have a `kid` that isn't in the published JWKS, that's a platform-side rotation problem and worth a support ticket with the project ref and a sample (redacted) token header.
This is unfortunately a platform-side repair, not something you can fix from the project owner role — and your diagnosis is basically correct about the shape of it. "Database error saving new user" with `auth.users` at 0 rows after a pause/resume is the classic sign that the `auth` schema didn't come back fully consistent: the auth service tries to run the signup insert (which includes its internal trigger/migration machinery), and a missing/short `auth.schema_migrations` state makes every insert hard-fail. The reason you get "permission denied" trying to patch `auth.schema_migrations` yourself is that the whole `auth` schema is owned by `supabase_auth_admin`, and project owners deliberately don't have write access there — so you can't apply the fix even though you've correctly guessed what it probably is. So realistically: - This needs Supabase support to repair the `auth` schema state on the instance — keep the ticket as the primary channel, and add the project ref + your `count(*)` diagnostics (they're genuinely useful, it'll speed up triage). Reply on the existing ticket rather than opening new ones so you don't lose your place in the queue. - I'd **stop firing more signups** in the meantime — they'll keep hard-failing and won't self-heal. - Don't force writes to `auth.*` even if you find a way — a half-applied auth migration is worse than the current state. If the project is early enough that there isn't much to lose, the fastest unblock is often to spin up a **fresh project** and migrate your `public` schema + data into it, rather than waiting on the repair — but if you've got real users/data tied to `auth.users`, sit tight on the ticket and let support fix the instance.
You're right that you can't touch the `supabase_admin`-owned default-privilege entries — `ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin ...` needs membership in that role, which you don't get on hosted. So chasing the `pg_default_acl` rows is a dead end. The cleaner lever is one level up: **revoke schema `USAGE` from `anon`** instead of trying to keep every future object's grants clean. ```sql revoke usage on schema public from anon; revoke create on schema public from anon; ``` `anon` needs `USAGE` on the schema to touch *anything* inside it, regardless of per-object grants. Once that's gone, it doesn't matter that new tables/functions are born with `anon` privileges — `anon` can't reach them, because it can't use the schema at all. That gives you exactly the posture you want (only `authenticated` + `service_role`) without fighting the default ACLs object-by-object. Two caveats: - This nukes `anon` access to `public` entirely, so make sure nothing legitimately relies on the anon/PostgREST-unauthenticated role for `public` (sounds like that's your intent). - If you ever *do* want a specific object reachable by `anon`, you'd re-grant `usage` on the schema, which reopens the general path — at which point RLS becomes your gate again. If you'd rather keep schema `USAGE` for some reason, the belt-and-suspenders alternative is a DDL event trigger (`ddl_command_end` on `CREATE TABLE`/`FUNCTION`/`SEQUENCE`) running a `SECURITY DEFINER` function that `REVOKE`s `anon` on each newly created object — but honestly the schema-level revoke is simpler and closes the hole completely.
The "a *previous* RLS-denied upload makes a *later* upload hang" pattern almost always points at the HTTP transport rather than Storage itself — specifically an error response whose body never gets drained, leaving a keep-alive socket in a bad state that the next request then blocks on. Bun's `fetch` has had a few sharp edges here. When the RLS check denies the upload, storage-js gets a non-2xx response. If that error body isn't fully consumed, the underlying connection can be reused in a half-read state, and the next upload on that connection just sits there. A signed-out client in `afterEach` doesn't help because the transport/agent is process-global, not per-client. Things worth trying, roughly in order: 1. **Rule out Bun** first: run the same test file under Node + `vitest` (or `node:test`). If it passes there, you've confirmed it's Bun's fetch, and the discussion moves to a Bun issue rather than Supabase. 2. **Force connections closed** in the test env so a poisoned socket can't be reused — e.g. send `Connection: close`, or disable keep-alive for the test run. 3. **Drain the error response**: after the denied upload, make sure the error path actually reads the body (storage-js should, but if you're catching early, the body may be left dangling). 4. Add a hard timeout to the hanging upload so a stuck socket fails fast instead of hanging the whole suite, which also makes the repro clearer. If it turns out to reproduce under Node too, then it's more likely the self-hosted Storage API (or rustFS) holding a lock on the object key after the denied attempt — in that case the storage container logs around the denied request vs. the hung one would be the next thing to compare. Do you see it under Node/vitest, or only Bun?
Nice isolation work — "either feature alone is fine, only the combination breaks" is a strong signal this is a genuine PostgREST bug rather than a grants/schema issue, and your `information_schema` checks back that up. What's happening under the hood: on a mutation with `return=representation`, PostgREST wraps the write in a CTE and then projects the `select=` columns from it. The `or=(...)` filter's column reference ends up being resolved against that returning projection/CTE scope rather than the base table, so a column that exists on the table reads as "does not exist" (42703). A plain `eq` filter doesn't trip it because of how the simpler predicate is planned; the `or` group is what lands in the wrong scope. Workarounds until it's fixed upstream: - **Split the call:** do the `.update(...).or(...)` **without** `.select()`, then issue a separate `.select()` for the affected rows. Two round-trips, but it sidesteps the bad combination. - **Reshape the filter:** if your `or` is expressible as an `in.(...)` or a single predicate, that avoids the `or` group entirely. - **Push it into an RPC:** a small `plpgsql`/`sql` function that does the `UPDATE ... RETURNING` gives you the atomic write + representation without going through PostgREST's filter+returning combination. Since this is reproducible and well-scoped, it's worth filing on the PostgREST repo (PostgREST/postgrest) with your minimal curl repro — it'll get triaged faster there than in Discussions. If you can include the PostgREST version (Dashboard → Project Settings → Infrastructure, or the `Server` header on a REST response) that'll help them pin the affected range.
The detail that it also reproduces in the SQL editor (not just PostgREST) is important — that rules out the pooler/prepared-statement/plan-cache angle from your question (1) and points at the function boundary itself. The usual culprit when a `LANGUAGE sql` function behaves differently from the same body inlined and from a `plpgsql` copy is **`search_path` + operator resolution**. With `proconfig NULL`, your `sql` function inherits the caller's `search_path`, but a simple `LANGUAGE sql` function is a candidate for **inlining**, and the operator/opclass resolution for pgvector's `<=>`/`<->` gets pinned relative to the schema search path at that point. If `extensions` isn't consistently on the path the moment the planner resolves the operator inside the function, you can silently get a different distance operator (or a different plan) than the inlined `SELECT` you ran by hand — which is exactly the kind of thing that returns 0 rows instead of 3, while your hand-inlined query and the `plpgsql` version (which is *not* inlined and re-plans on execute) both work. Two concrete things to try: 1. Pin the path on the function and see if it changes: ```sql alter function public.match_kb_chunks(vector, integer) set search_path = public, extensions; ``` 2. Schema-qualify the operator/opclass in the body (e.g. use `operator(extensions.<=>)` and the explicit `vector_cosine_ops` opclass) so resolution can't drift. For diagnosis, `EXPLAIN (VERBOSE, ANALYZE)` the function call vs. the inlined query and compare — I'd bet one is doing an index scan with the vector opclass and the other isn't. If pinning `search_path` fixes it, that was it; if not, sticking with the `plpgsql` version you already have is a perfectly legitimate workaround (it forces a fresh plan and avoids the inlining path entirely). If you can paste the function body and the WHERE/ORDER BY that uses the vector operator, I can point at the exact line.
The fact that raw TCP opens but the Postgres/TLS handshake never completes, while the Management API can still query the DB, is the useful clue here — the Management API talks to your database over a completely different internal path, so it staying green doesn't tell you much about the pooler's external listener. What you're describing is the pooler (Supavisor) accepting the socket and then stalling before the startup packet completes. A few things to check/try before assuming it's an incident: - **Username format.** On the pooler you must connect as `postgres.qykgmpwyztxvyyfzdgds` (role + project ref as the tenant), not bare `postgres`. A wrong tenant can get you a socket that then hangs on the startup exchange. - **Prisma + pooler mode.** Port `5432` on `...pooler.supabase.com` is session mode, `6543` is transaction mode. `prisma migrate`/`db push` need session mode or (better) a **direct** connection, because migrations use advisory locks and prepared statements that transaction pooling doesn't support. Point Prisma's `directUrl` at the direct connection (`db.<ref>.supabase.co:5432`, or enable the IPv4 add-on if you're IPv4-only) and keep the pooler for the app's `url` with `?pgbouncer=true`. - **SSL.** Make sure the connection string has `sslmode=require` — a missing TLS param behind the load balancer can look exactly like "connects then hangs". If you've already got the username/port/sslmode right and a **direct** connection also stalls while internal Supavisor auth keeps succeeding, then it really is pooler/LB-side for your project and there's nothing you can fix from the client — open a support ticket with the project ref (this overlaps with the pooler issues some eu-central projects saw starting late June). If you can share your exact connection string shape (host, port, username pattern, sslmode) I can sanity-check it.
Perfect, that pins it down a lot. Two things stand out. **First, an important distinction about *where* that error comes from.** `Unable to exchange external code` is a **server-side** GoTrue error for the *Supabase ↔ Google* exchange — it happens at `https://<ref>.supabase.co/auth/v1/callback`, **before** the browser ever gets back to your SPA and runs `exchangeCodeForSession(...)`. GoTrue then redirects to your app with `?error=server_error&error_description=Unable+to+exchange+external+code`. So this particular failure is *upstream* of your callback code — which is why fiddling with the client-side exchange hasn't fixed it. Since you've already verified the redirect URI and the client ID/secret, the remaining cause of that server-side message is almost always **Google's single-use authorization code being consumed twice** — and your **Cloudflare Worker** is the prime suspect: - Cloudflare features that **pre-fetch or scan links** will hit the callback URL before (or in addition to) the user's browser and burn the one-time `code`: **Speed → Optimization → Prefetch/Preload**, **Email Link scanning**, **Bot Fight Mode**, and browser speculative prefetch. Turn those off for the auth callback path and retest. - If any **Worker route sits on the redirect path** and does a `fetch()` of the callback URL (or a redirect that gets followed twice), that also consumes the code. The auth callback should pass straight through to Supabase / your SPA with no Worker-side fetch. Quick way to confirm: open DevTools → **Network**, keep "Preserve log" on, do the Google login, and look at how many times `/auth/v1/callback` (and the Google `oauth2` endpoint) are requested. If it's **more than once**, that's your bug. **Second — the allow-list you weren't sure about.** In the dashboard: **Authentication → URL Configuration**. Your **Site URL** and the exact SPA callback URL must be in **Redirect URLs** (wildcards allowed, e.g. `https://app.example.com/**`). If the `redirectTo` you pass to `signInWithOAuth` isn't listed, GoTrue won't complete the round-trip cleanly. **Then fix the client side (a separate bug that will bite once the above is resolved).** With `@supabase/supabase-js` and PKCE, the client defaults to `detectSessionInUrl: true`, so **supabase-js already exchanges the code automatically** when your callback page loads. Calling `exchangeCodeForSession()` yourself on top of that is a *second* exchange of the same code → guaranteed failure. Also, `exchangeCodeForSession` expects the **`code` value**, not the whole URL. Pick one approach: - **Recommended (let the client do it):** delete the manual call. On your callback route just wait for the session, then navigate: ```ts useEffect(() => { const { data } = supabase.auth.onAuthStateChange((event, session) => { if (session) router.navigate({ to: "/admin" }); }); return () => data.subscription.unsubscribe(); }, []); ``` - **Or do it manually** — then set `detectSessionInUrl: false` in `createClient(url, key, { auth: { detectSessionInUrl: false, flowType: "pkce" } })` and pass just the code, once: ```ts const code = new URLSearchParams(window.location.search).get("code"); if (code) await supabase.auth.exchangeCodeForSession(code); ``` One more SPA gotcha: the PKCE `code_verifier` is stored in the **localStorage of the browser/tab that started the login** — so the whole flow has to finish in that same browser (which also explains why "different device/mobile" didn't help; each device starts its own verifier). If you grab the Network trace (how many callback hits, and the exact `error_description` on the redirect), I can tell you definitively whether it's the Cloudflare double-hit or the allow-list.
Good catch — those really are two different problems, so let me split them. **1. "Unable to exchange external code"** — this is the one actually blocking login. It's a Supabase Auth (GoTrue) error that happens **server-side**, when Supabase tries to exchange Google's one-time authorization code. It is **not** a CORS issue — CORS failures show up in the browser console as blocked requests (`No 'Access-Control-Allow-Origin'`), not as a code-exchange error. Usual causes, in order: - **Redirect URI mismatch.** In the Google Cloud console, the *Authorized redirect URI* must be exactly your Supabase auth callback: `https://<your-project-ref>.supabase.co/auth/v1/callback` (or, if you self-host, `https://<your-gotrue-domain>/auth/v1/callback`). One character off and the code can't be exchanged. - **Code used twice / expired.** Google's `code` is single-use. If your callback runs twice (React StrictMode double-effect, a link prefetch, or a page refresh), the second exchange fails with exactly this message. Make sure `exchangeCodeForSession` runs only once. - **Wrong Google Client ID / Secret** in the provider settings (Dashboard → Authentication → Providers → Google, or `GOTRUE_EXTERNAL_GOOGLE_*` if self-hosted). - **Site URL / Redirect URLs not allow-listed** (Authentication → URL Configuration). **2. "Login succeeds but loops back to /auth or /admin"** — this is the session-not-persisted case from my note above: the session isn't written where your route guard reads it, so the guard bounces you straight back. To give you exact "how do I apply this" steps, three things would help: 1. Are you on **Supabase Cloud** (`*.supabase.co`) or **self-hosted**? (Changes where the config lives.) 2. What's the frontend — plain React/Vite SPA, Next.js, something else — and which Supabase library (`@supabase/supabase-js` alone, or `@supabase/ssr`)? 3. Is the code exchange happening in the **browser** or in a **server route**? Quick version while you confirm: - **SPA (browser / PKCE):** don't hand-roll the exchange. With `detectSessionInUrl: true` (the default), `supabase-js` finishes the callback automatically when the user lands back on your redirect page — just make sure they return to a page that instantiates the Supabase client, **in the same browser** the login started in (the PKCE `code_verifier` is stored there). - **Server (SSR, e.g. Next.js):** use `@supabase/ssr`, and in your `/auth/callback` route call `exchangeCodeForSession(code)`, let it write the cookies, then redirect to `/admin` — the snippet in my comment above. Given the "Unable to exchange external code" error, I'd start by checking the Google **Authorized redirect URI** matches `.../auth/v1/callback` exactly, and that the callback isn't firing twice. If you confirm 1–3 I can point you at the exact file/line to change.
Having the project ref, local source, `.env`, and `config.toml` is helpful evidence, but restoring account/project access can only be done by Supabase support — not via the forum. Open a ticket at https://supabase.com/dashboard/support with the **project ref**, the **organization billing email**, and card **last-4** if on a paid plan, and mention it's a production app. One practical safeguard in the meantime: if your `.env` still has a working database connection string (or the DB password), run a `pg_dump` now to secure a local backup of your data while the access issue is being sorted out.
If you signed up with **GitHub or Google** (OAuth), your Supabase account email is whatever address is primary on that provider — check which GitHub/Google identity you used to sign up, and log in with that same provider button. If it was **email + password**, there's no way to look up/enumerate the address from the outside. In that case contact Supabase support with any evidence you own the account — project ref, the organization's billing email, and card last-4 if on a paid plan — and they can help identify and recover it.
If you genuinely never enrolled MFA, this looks like an account/bug case rather than something you can self-serve, so it has to go through support. Reply to your ticket (SU-404571) and ask them to **inspect and clear any MFA factor on the account**, attaching proof of ownership (account email, project ref, org billing email, card last-4). One sanity check while you wait: confirm you're actually landing on the intended account/org and not a second account that got created via GitHub SSO under the same email — a duplicate SSO account can look exactly like a "spurious" MFA prompt. But if it's the right account, only support can remove the factor.
This is the catch-22 where the recovery form itself demands the MFA you've lost ("Insufficient AAL"). The in-dashboard form won't work while you're locked out, so don't rely on it — instead **email Supabase support directly** from the email tied to the account, explain you've lost your TOTP device with no backup/recovery codes, and quote the "Insufficient AAL: MFA required" error so they know why the in-product route dead-ends. Include proof of ownership: account email, project ref, org billing email, and card last-4 if on a paid plan. Only support can remove the MFA factor — the community can't. Once you're back in, re-enroll MFA and **save the recovery codes** this time so you're not stuck again.
A 522 on REST/Auth means the compute instance is unreachable, and a restart failing with a "capacity" error means the region can't currently place your instance — both point to a platform-side capacity issue rather than your config. Two things worth flagging: - **Don't downsize to MICRO.** Less memory makes health/OOM worse, not better; if anything you'd want to go back up once it's placeable. - Real recovery here is Supabase-side (they need to place or migrate the instance), so the priority is to **escalate your support ticket** — replying to it bumps it, and Pro includes email support. As a fallback for a hard-down production project, you can restore your latest backup / PITR into a **new project in a different region** to get running again while the original is recovered. This is very likely related to the multi-region status-change incident happening around the same time.
This lines up with the concurrent platform incident ("Project status change failures in multiple regions") — new-project **creation** was degraded in several regions even while the region's runtime looked green, and the public status page tends to lag the incident. So it's platform-side, not anything wrong with your setup. Check https://status.supabase.com for the active incident and simply retry creating the project once it's marked resolved. If it's still failing well after the incident clears, open a support ticket noting the region (eu-west / Ireland) and the exact error you get.
MFA lockouts can only be resolved by Supabase support, so this needs to go through your ticket rather than the forum. A few tips to get SU-403868 moving: - **Reply to the existing ticket** to bump it rather than opening new ones (duplicates reset your place in the queue). - Attach clear proof of ownership: account email, project ref, org billing email, and card last-4 if on a paid plan. - If you're on a **paid plan**, mention it — paid tiers get faster support SLAs. Late June saw a heavy ticket backlog, which is likely why the response was delayed. Keep everything on the one thread so an agent can pick it up in context.
Whether this is recoverable depends on your plan's backups — act quickly, since retention is limited: - **PITR enabled** (paid add-on): restore to a timestamp just before the drop via **Database → Backups → Point in time**. - **Paid plan without PITR**: use the most recent **daily backup** (Database → Backups). You'll lose changes made since that snapshot. - **Free tier**: there are no managed backups, so recovery generally isn't possible unless you have your own `pg_dump`. You've already done the right thing by stopping writes. Also open an **urgent** support ticket with your project ref referencing the accidental drop — if any snapshot exists on their side, support can help you restore it. Going forward, enabling PITR is worth it for production data.
That banner corresponds to a platform incident scoped to **project status-change operations** (create / pause / resize / restore) across multiple regions — not necessarily to already-running databases. So: - Your tables and data still being visible is a good sign the database itself is up and serving. - The main risk during the window is triggering a state change (pausing, restoring, resizing, or creating projects), which is what's degraded — best to avoid those until it clears. - Connections / env vars / integrations (e.g. Lovable) should keep working as long as the DB is healthy. There's no community-side ETA — track the specific incident and affected regions on https://status.supabase.com. If your project is actually unreachable (not just the banner), open a support ticket with your project ref.
MFA resets can only be done by Supabase support — nobody in the community can do it. Since you already have a ticket (SU-411628), the best thing is to **reply to that existing ticket** rather than opening new ones (duplicates tend to push you back in the queue), and attach proof of ownership: account email, project ref, and the org billing email / card last-4. If the in-dashboard support form won't submit while you're locked out, email support directly and quote the ticket number in the subject.
Access/ownership recovery can only be actioned by Supabase support — the community forum can't restore account or project access. Open a ticket at https://supabase.com/dashboard/support (or email support@supabase.io) and include proof of ownership so they can verify you quickly: - the **project ref** (and project URL), - the **organization's billing email**, - last-4 of the payment card if you're on a paid plan. Put "urgent / production access lost" and the project ref in the subject to help triage. If you still have the database connection string in an old `.env`, consider taking a `pg_dump` now to secure your data while the account issue is resolved.
This is the same issue as #47507. Two parts: 1. To get an **OTP code** instead of a magic link, edit **Auth → Email Templates → Magic Link** and add the `{{ .Token }}` variable — the default template only has `{{ .ConfirmationURL }}`, which is why you only receive a link. 2. If email delivery itself fails, the built-in SMTP is rate-limited and mainly delivers to team addresses — configure **custom SMTP** under **Auth → SMTP Settings** for reliable delivery.
Agree this would be handy. It's CLI-only right now — you can already include data with `supabase branches create --with-data` (or when creating a persistent branch), so a workaround is to create branches via the CLI instead of the GitHub integration, or seed the branch with a migration/seed file. A toggle under **Settings → Integrations → GitHub** is a valid ask — worth upvoting so it gets tracked on the roadmap.
There isn't an equivalent to this in the Table Editor today beyond manually dragging column widths, so this is a reasonable Feature Request — upvoting the thread helps the Studio team gauge demand. As a workaround for wrangling big tables in the meantime, the SQL Editor lets you control exactly what you see, e.g. shortening UUIDs so rows stay compact: ```sql select substring(id::text, 1, 6) || '…' as id, name, created_at from public.your_table limit 100; ```
A login loop after OAuth almost always means the session isn't being persisted after the redirect. If you're using `@supabase/ssr`, you have to exchange the code for a session in your callback route and write the auth cookies: ```ts // app/auth/callback/route.ts const { searchParams } = new URL(request.url) const code = searchParams.get('code') if (code) await supabase.auth.exchangeCodeForSession(code) ``` Also check: - **Auth → URL Configuration**: Site URL + Redirect URLs contain your exact domain. - The callback handler runs **server-side** and actually sets the cookies. - Third-party cookie blocking isn't dropping the session (test in incognito). - No clock skew on the machine. If the loop is on the **Supabase dashboard itself** (not your own app), it's usually browser-side: try incognito, disable extensions, and allow cookies for supabase.com.
You can't change a project's region in place — you'll need to migrate to a new project: 1. Create a new project in the **eu-central (Zurich)** region. 2. Dump the old database and restore into the new one: `supabase db dump -f dump.sql`, then restore it (or use `pg_dump` / `pg_restore` against the two connection strings). 3. Migrate `auth.users` (dump/restore the `auth` schema), move your Storage objects, and re-point your API keys / project URL in the app. There's no one-click region move today, so plan a short maintenance window for the cutover.
Two separate things are going on here: 1. **You get a link, not a code.** The Magic Link email template sends a link by default. To receive a numeric OTP, edit **Auth → Email Templates → Magic Link** and add the `{{ .Token }}` variable — the default template only contains `{{ .ConfirmationURL }}`. Once the template includes `{{ .Token }}`, the code returned by `signInWithOtp` works with `verifyOtp`. 2. **"Error sending magic link email"** is almost always the built-in SMTP limit. The default sender is heavily rate-limited and only reliably delivers to your own team addresses. Set up **custom SMTP** under **Auth → SMTP Settings** for real usage, and check the Auth rate limits.
There's no SQL for this, because saved SQL Editor snippets don't live in your Postgres database — they're stored in the Supabase **platform** (per user), so there's no table you can `DELETE from`. The dashboard only lets you remove them one at a time. If you want to bulk-delete them, script it against the platform content API. The dashboard itself issues a request like `DELETE https://api.supabase.com/.../projects/{ref}/content?id=<snippetId>` — open your browser devtools → **Network** tab while you delete a single snippet, copy that request, and loop it over the IDs with a personal access token. A proper bulk-delete option would be a good Feature Request.
This is a known, benign advisor finding rather than something you need to "fix". `spatial_ref_sys` is a PostGIS **system reference table** (SRID definitions) owned by the extension — you generally can't enable RLS on it, and forcing RLS on it can break PostGIS. Recommended options, roughly in order of preference: 1. **Accept / ignore** the warning — the table holds reference data, not user data. 2. If you don't want it exposed through the auto-generated API: ```sql revoke all on public.spatial_ref_sys from anon, authenticated; ``` 3. Cleanest long-term: install PostGIS into a dedicated `extensions` schema instead of `public`, so the advisor stays quiet and `public` only contains your own tables. I would not try to enable RLS directly on `spatial_ref_sys`.
Since Realtime enforces RLS on `postgres_changes`, the usual cause of "SUBSCRIBED + heartbeats fine but zero change events" is that the socket is connected **anonymously** — with `@supabase/ssr` the browser client often doesn't have the user's access token attached, so RLS silently drops every event. Things to check, in order: 1. **Set the auth token on the realtime client** after sign-in (and again on token refresh): ```ts supabase.realtime.setAuth(session.access_token) ``` And make sure you subscribe from the **browser** client that holds the session — not a server component. 2. **`REPLICA IDENTITY FULL`** on the table (needed for UPDATE/DELETE payloads and for RLS to evaluate the row): ```sql alter table public.projects replica identity full; ``` 3. **Publication** includes the table for the ops you want: ```sql alter publication supabase_realtime add table public.projects; ``` 4. Confirm Realtime is enabled for the table in **Database → Replication**. In practice the vast majority of these reports come down to #1. Hope this helps!