Skip to content

Query Supabase project logs through the Management API after logs.all

Supabase is removing the Management API’s analytics/endpoints/logs.all endpoint on 2026-09-23.1 Anything that queries project logs programmatically - a log-tail script, a SIEM pull, an audit check - has to move to the replacement analytics/endpoints/logs endpoint before then. Dashboard log exploration is unaffected.

The migration is more than a path rename: the backend moved from BigQuery to ClickHouse, per-source tables collapsed into one unified logs table, and nested-field access changed shape. And the official migration guide has a bug in its own example - it tells you to filter by source_name, a column that does not exist. Everything below was measured against the live endpoint on 2026-08-22 on a project with real traffic.

The one thing the migration guide gets wrong

Section titled “The one thing the migration guide gets wrong”

The changelog’s worked example says:

SELECT timestamp, event_message
FROM logs
WHERE source_name = 'edge_logs'
ORDER BY timestamp DESC
LIMIT 100

Run that against the real endpoint and it fails:

{"result": null, "error": "Field \"source_name\" does not exist."}

The column is source. The OpenAPI description of the endpoint says source;2 the changelog example is the outlier. The working form:

SELECT timestamp, event_message
FROM logs
WHERE source = 'edge_logs'
ORDER BY timestamp DESC
LIMIT 100

If you migrated early by following the guide verbatim and concluded the endpoint was broken, this is why.

The endpoint is GET-only (POST returns a 404), takes the SQL in the sql query parameter, and requires an explicit timestamp window - on 2026-08-22 a query without one defaulted to the last minute; on 2026-09-02 it answered the generic backend error instead (trap 6 below) - and a window cannot span more than 24 hours:

Terminal window
REF=<project-ref>
curl -s -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
"https://api.supabase.com/v1/projects/$REF/analytics/endpoints/logs?sql=$(jq -rn --arg q "SELECT timestamp, event_message FROM logs WHERE source = 'auth_logs' ORDER BY timestamp DESC LIMIT 50" '$q|@uri')&iso_timestamp_start=$(date -u -d '15 minutes ago' +%Y-%m-%dT%H:%M:%SZ)&iso_timestamp_end=$(date -u +%Y-%m-%dT%H:%M:%SZ)"

Timestamps need a timezone suffix (...T%H:%M:%SZ); a bare 2026-08-22T00:00:00 is rejected with Invalid ISO datetime.

For a pull longer than a day, slice the range into windows of at most 24 hours (the cap the endpoint imposed on 2026-08-22; not in the evidence table) and issue one query per window, ordered by timestamp with an explicit LIMIT. Whether LIMIT has a ceiling, and whether any pagination exists beyond time-slicing, was not measured.

Three conversions cover every query that worked on logs.all.

Per-source table to a source filter. There is no edge_logs or auth_logs table anymore - every source lives in the one logs table, keyed by the source column. Sources observed on live projects: edge_logs, postgres_logs, pgbouncer_logs, auth_logs, storage_logs, realtime_logs, postgrest_logs, and function_logs (Edge Function console.log lines, measured 2026-09-02). A quick inventory of what a project actually emits:

SELECT source, count() AS n FROM logs GROUP BY source ORDER BY n DESC

unnest(metadata) to log_attributes map access. BigQuery-era queries reached nested fields by unnesting the metadata array one level at a time. Those joins are gone - fields are flat keys on a log_attributes map:

-- before (BigQuery, logs.all)
SELECT timestamp, r.method
FROM edge_logs
CROSS JOIN unnest(metadata) AS m
CROSS JOIN unnest(m.request) AS r
-- after (ClickHouse, logs)
SELECT timestamp, log_attributes['request.method'] AS method
FROM logs
WHERE source = 'edge_logs'

For Postgres logs the parsed fields keep their parsed. prefix: log_attributes['parsed.error_severity'], log_attributes['parsed.backend_type'].

Dialect, not just shape. The SQL is ClickHouse now. count() not count(*), ClickHouse date functions, and so on. An old BigQuery query run unchanged against the new endpoint fails - but see the error-handling trap below, because it does not fail the way you expect.

#Trap
1Errors return HTTP 200. A dialect or parse failure is a {"result": null, "error": "Backend error! Retry your query."} envelope with status 200. If your script checks the status code only, every query failure looks like a transient blip - and the generic “Backend error” text is the same for a bad dialect, a bad column, and a genuinely transient failure. Parse the error field, then tell the cases apart: on Backend error! Retry your query., re-run a known-good sentinel (SELECT count() FROM logs with a window; measured returning a count) before retrying the original. If the sentinel succeeds, the failure is in your query and a retry loop never ends; if the sentinel fails too, back off and retry.
2SELECT * fails. The endpoint requires an explicit column list; SELECT * FROM logs LIMIT 1 returns the generic backend error. The usable columns include at least timestamp, id, event_message, source, log_attributes.
3timestamp changed type. logs.all returned microsecond integers; the new endpoint returns ISO strings ("2026-08-22T00:13:07.000000"). A parser doing new Date(ts / 1000) silently produces garbage dates instead of erroring. The string also carries no offset: the window you send is UTC, so parse the result as UTC (append Z) rather than letting a parser default to local time. The zone of the returned value was not tested separately from the input window.
4No deprecation or sunset header. logs.all serves BigQuery queries today exactly as before, with no Deprecation/Sunset header announcing the removal. Monitoring for deprecation headers will not warn you about this one - the changelog is the only signal.
5Rate limit is 10. x-ratelimit-limit: 10 per window on both endpoints, so a polling loop that worked at the old budget needs spacing out - and one query per source spends 8 of the 10 on the inventory above before any alert query runs. Poll with one query per window (GROUP BY source, or source IN (...)) instead of one per source. The window length and any x-ratelimit-remaining header were not recorded.
6The time window stopped being optional, and its absence is the same generic error. On 2026-09-02 all four queries sent with neither iso_timestamp_start nor iso_timestamp_end (a query carrying only one of the two was not tried), the WHERE source = 'edge_logs' SQL under “The one thing the migration guide gets wrong” among them, answered Backend error! Retry your query.; the same SQL with a three-hour window returned rows. Trap 1 makes this indistinguishable from a dialect mistake, so send the window every time. On 2026-08-22 that SQL ran without a window and the endpoint defaulted to the last minute; treat the requirement as current behaviour rather than a documented contract.

The old endpoint rejects ClickHouse SQL (count() fails on logs.all), so you cannot write one query that works on both - during a migration window, run the two dialects side by side.

Put the new-endpoint query into whatever scheduled check or SIEM pull runs today, alongside the old one, and compare the two result sets. logs.all carries no Deprecation or Sunset header (trap 4), so nothing in the response will remind you. Done this way, 2026-09-23 is the day you delete the old query.

For anything built on the auth log stream, the unified table preserves the structured detail. A failed login, a failed refresh, and a signup against an existing email were each captured within a minute under source = 'auth_logs', with the full attribute set flat on the log_attributes map: error_code, grant_type, method, path, remote_addr, referer, request_id, status, duration. The same JSON also arrives in event_message, so either extraction path works:

SELECT timestamp, log_attributes['error_code'] AS error_code,
log_attributes['remote_addr'] AS remote_addr
FROM logs
WHERE source = 'auth_logs'
ORDER BY timestamp DESC
LIMIT 50
ClaimStatusHow it was checked
source_name does not exist; the column is sourcemeasuredGROUP BY source_name -> Field "source_name" does not exist; GROUP BY source returns per-source counts, live project, 2026-08-22
New endpoint is ClickHouse-onlymeasuredSELECT count() FROM logs -> 200 with count; same query shape on logs.all -> generic backend error
GET-onlymeasuredPOST to /analytics/endpoints/logs -> 404 Cannot POST
SELECT * failsmeasuredgeneric backend error; explicit column list succeeds
unnest(metadata) no longer worksmeasuredgeneric backend error on the old pattern; log_attributes['key'] returns values
timestamp is ISO string, not microsecond intmeasuredresponse rows on both endpoints compared
iso_timestamp_start and iso_timestamp_end are requiredmeasured 2026-09-02four queries with neither parameter (edge_logs, function_logs, with and without LIKE; a query with only one of the two was not tried) -> generic backend error; the same function_logs query with a three-hour window -> rows (source function_logs carries Edge Function console.log lines)
Errors return HTTP 200measuredevery failing query above was status 200 with an error body
Auth events land in the unified table with full attributesmeasuredgenerated failed-login, nonexistent-user, duplicate-signup, and bad-refresh events; all four appeared under source = 'auth_logs' within a minute
Rate limit 10, no deprecation headermeasuredresponse headers on both endpoints
  1. Supabase, “Migration of Supabase Management API logs.all analytics endpoint to logs endpoint,” Supabase Changelog, 2026-07-23. https://supabase.com/changelog/48235-migration-of-supabase-management-api-logs-all-analytics-endpoint-to-logs-endpoint

  2. Supabase, “Get project logs,” Management API reference. https://supabase.com/docs/reference/api/v1-get-project-logs