Debugging & Troubleshooting
The one skill that matters most on Supero: telling apart responses that look identical but mean different things. A 200 with an empty list has four causes; a connector run that says completed has four more. This page is the probe for each — what to check to know which one you are looking at.
Read this first
🚨 The question is almost never "what does this error mean"
ℹ️ This page is symptom-first
Get a diagnosis handle
| Handle | How to get it | Why it matters |
|---|---|---|
| Your effective scope | GET /api/v1/auth/session | Assumed scope is wrong more often than assumed permissions. A token carries a domain, project and tenant, and that is what decides what you can see. This is step zero for the single most common problem — an empty list. |
| A correlation ID | Set your own: send X-Request-Id (or X-Correlation-ID) on the request, and quote that value. | The platform records every request under a correlation ID, and you can then read the trace with GET /api/v1/logs/trace/{id}. Setting your own is the reliable way to have one in hand — a server-generated ID is returned in the body only on a 500, so do not rely on being handed one. |
| The run record | GET /api/v1/connectors/{id}/status (last executions); workflow runs via their instance records | For anything asynchronous, the run record — not the HTTP response that started it — is the source of truth. It carries per-run status, row counts, and errors that never appear in a live response. |
⚠️ The console does not show you a correlation ID
A 200 with an empty list
| Cause | The one check | What confirms it |
|---|---|---|
| Your token's scope doesn't match where the records live | Re-issue the same request with the project and tenant set explicitly. | The rows appear. A domain-level token carries no implicit tenant, so nothing resolves until you name one. |
| Permission is acting as a filter, not a gate | Run the identical query as an admin. | The admin sees N rows, the caller sees 0. A read the caller isn't entitled to is narrowed to empty rather than refused with a 403. |
| A live-connector-backed schema hasn't resolved its binding | Look at the access mode on the schema, not the row count. Also compare a POST /query against the plain list for the same type. | The access mode is what tells you the source is live; a 0-row list with a live mode is an unresolved binding, not an empty table. If /query returns rows and the list returns none, they are taking different paths. |
| There genuinely are no rows | All of the above agree. | Only then is the empty list real. |
# "Why is my list empty?" — run top to bottom, stop when one explains it.
API=https://api.supero.dev/api/v1
# 1. What scope am I actually in? (the most common cause)
curl -s $API/auth/session -H "Authorization: Bearer $TOKEN" \
| jq '{domain:.domain_name, project:.project_name, tenant:.tenant_name}'
# 2. Re-run the list scoped explicitly to that project/tenant.
# Rows now? It was a scope problem.
curl -s "$API/crud/$DOMAIN/$TYPE?limit=5" -H "Authorization: Bearer $TOKEN" | jq '.result_count'
# 3. Is this schema backed by a live connector? A live mode + 0 rows is an
# unresolved binding, not an empty table — not something a re-query fixes.ℹ️ Some checks need more than a tenant login
A connector run says completed
| Cause | The one check |
|---|---|
| Incremental sync ran as a no-op | The connector's sync state has no advancing position per stream. An incremental connector that is working leaves a cursor that moves forward each run; an inert one completes without one. (Reading the sync state is a builder-level surface.) |
| The cursor field never changes | Confirm the field the connector tracks for changes is one that actually moves when a row is updated. A created-at column, for example, is set once and never changes, so every later update is missed — the sync completes and looks healthy while silently ignoring edits. |
| The run did less than it says | Compare the rows written against the source. A run marked partial, or one marked completed with a row count far below the source, did not drain it — often because it was cut short by a deployment. "Completed" means the run finished without raising, not that the source was fully read. |
| Rows landed under the wrong type | The rows exist, but under a different schema than you expected, with no failure recorded. A source table name that is not an exact match for the target type can route rows elsewhere. Check the other types in the namespace before concluding nothing arrived. |
⚠️ A passing connection test proves very little
404 Schema not found
| Cause | The one check |
|---|---|
| Propagation race | It clears on its own within a few seconds. A freshly uploaded schema is not instantly visible everywhere; a create fired immediately after can 404 although the upload succeeded. Retry with a short backoff before treating it as a real error. |
| The upload was rejected | The upload response itself reports a failure. Schema upload is all-or-nothing per batch: a couple of validation errors can produce zero created schemas and then dozens of downstream "schema not found" errors that are one event, not many. Read the upload response before chasing the CRUD errors. |
| A breaking-change re-upload was discarded | The upload returned success with zero uploaded and one or more failed. A re-upload that would break existing data is refused, but the response can still carry a success envelope — check the counts, not just the status. |
| Wrong address | The type resolves when addressed as namespace:type but not by a display or plain name. Object types are addressed as namespace:name — confirm you are using the namespace the schema was uploaded under, not a display label. |
A write was "denied" — or was it?
| Response | What it actually means |
|---|---|
| 403 | A real authorization denial. This is the response that means "you are not allowed." |
| 404 | Usually NOT an authorization denial. The type you addressed does not exist in this scope — a missing schema masquerading as a permission failure. Fix the type or the scope, not the policy. |
| 422 | Authorization already PASSED. Body validation happens after the scope check, so a 422 means the request got through and failed on its shape, not its permissions. |
💡 The admin control test
⚠️ A field-level policy strips, it does not always refuse
A value looks wrong: $0.00, or "not configured"
A money or metric field reads 0
"Integration not configured" — but you configured it
First check by layer
| Layer | First thing to check |
|---|---|
| Auth / scope | GET /api/v1/auth/session — is the domain, project and tenant what you expect? Most "permission" problems are scope problems. |
| Schema | Read the upload response, not the downstream errors. Confirm the schema is addressed as namespace:type and that the namespace matches the project's. |
| CRUD / write | Read the record back after writing. The response to a write does not always carry back every field you sent; a round-trip is the only reliable confirmation. |
| Query / list | Filters go through POST /query, not query-string parameters on a list. An unrecognised list parameter is dropped, and the list returns 200 unfiltered. |
| Connector | The run record (GET /api/v1/connectors/{id}/status), never the response that started the run. Look at status, row counts, and per-record errors. |
| Workflow | The run's step logs. An accepted workflow that wrote no step logs did not execute its steps. |
| Deployment | The deploy status and the deployed URL's own behaviour — a 200 from a deployed app is not proof it rendered your latest bundle. |
What you cannot see (so you stop looking for it)
- •There are no metrics. Request rates, latencies and error rates are not exposed as numbers anywhere — measure at your own client if you need them.
- •Nothing pushes to you. There is no alerting and no failure webhook; detection is something you poll for. A deploy still pending after about five minutes should be treated as failed.
- •A connector's underlying worker has no readable log surface. What is not written into the run record is not recoverable — so the run record is where to look, and its per-record errors are your only durable detail.
- •You cannot read a published app's container logs through the platform. Debug a deployed app from its own behaviour and the data it reads and writes, not from server logs.
- •If two identical calls return different results, a caching difference between server instances is a likely cause; retrying is the practical response.
Verify behaviour, don't trust a green check
- •After a write, read the record back and diff it against what you sent. This catches stripped fields, defaulted-over values, and fabricated-looking successes in one step.
- •After a filtered read, prove the filter with a value you know matches nothing. A filter that returns rows for an impossible value is being ignored — the endpoint is returning everything with a 200.
# Prove a list filter is actually applied.
# If BOTH calls return rows, the filter is being ignored — filter via POST /query instead.
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.supero.dev/api/v1/crud/$DOMAIN/$TYPE?limit=1" | jq '.result_count'
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.supero.dev/api/v1/crud/$DOMAIN/query" \
-d '{"type":"'$TYPE'","filters":{"a_field":{"eq":"a-value-that-cannot-exist"}},"limit":1}' | jq '.result_count'
# expected: the second returns 0. If it returns your whole table, the filter shape is wrong.Reporting an issue
ℹ️ Where to report
- •Your domain, the project, and the tenant — the effective scope from GET /api/v1/auth/session.
- •The exact method, path, and timestamp, and the full response body and status.
- •A correlation ID if you have one — the reliable way is to have set your own X-Request-Id on the failing call.
- •For a connector or workflow: the run record, not just the response that started it.
- •What you expected versus what you got — including, for a silent failure, the read-back that showed the effect was wrong.
Next steps
The tools
Operations & Observability
Correlation IDs, log queries, the audit trail, and run histories in full. Read it
The list trap
Querying & Pagination
Why filtering is a POST, and the operators /query accepts. Read it
The status codes
Errors & Status Codes
The stable error codes to branch on, and what each status actually means. Read it
Before customers
Going to Production
The permissive defaults and seeded credentials a fresh app ships with. Read it
On this page