S
supero.docs
Developers/Platform Overview/Debugging & Troubleshooting

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

Most things that go wrong on Supero do not return an error. They return a success: HTTP 200, a connector run marked completed, an empty list, a write that reports 201. The platform is strict at the edges and forgiving in the middle — an unrecognised filter is dropped rather than rejected, a field you may not write is stripped rather than refused, a create that matched nothing still reports success.

🚨 The question is almost never "what does this error mean"

It is "which of several identical-looking success responses am I actually getting, and what one check tells them apart." A page of results that looks right can be the wrong tenant's data; a list that looks empty can mean four different things. This page is organised around those look-alikes, because that is where the time goes.

ℹ️ This page is symptom-first

For the tools themselves — correlation IDs, log queries, the audit trail, run histories — see Operations & Observability. This page assumes you have a symptom and want the fastest path to which cause it is.

Get a diagnosis handle

Three things make every step below faster. Get them before you start guessing.
HandleHow to get itWhy it matters
Your effective scopeGET /api/v1/auth/sessionAssumed 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 IDSet 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 recordGET /api/v1/connectors/{id}/status (last executions); workflow runs via their instance recordsFor 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

Today the in-product UI does not surface the correlation ID on an ordinary error. If you are debugging from the console rather than the API, reproduce the call against api.supero.dev with your own X-Request-Id header so you have an ID to trace and to quote in a report.

A 200 with an empty list

The most common symptom on the platform, and the one with the most causes. A list endpoint returns 200 and no rows. It is one of five things, and they are cheap to tell apart in order.
CauseThe one checkWhat confirms it
Your token's scope doesn't match where the records liveRe-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 gateRun 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 bindingLook 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 rowsAll of the above agree.Only then is the empty list real.
These are ordered checks, not exclusive causes — more than one can be true at once, so run them top to bottom and stop when one explains what you see. The first — scope — is the answer far more often than the rest combined.
bash
# "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

Comparing what an admin sees against what the caller sees needs an admin session. Reading a connector's sync state needs a builder/developer role. If you are inside a generated app as an ordinary user, the checks you can always run are your effective scope, the schema's access mode, and the run record — lean on those.

A connector run says completed

"Completed" means the run finished, not that it did what you wanted. A completed run with zero rows and zero failures is ambiguous — it is one of four things, and the run record distinguishes them.
CauseThe one check
Incremental sync ran as a no-opThe 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 changesConfirm 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 saysCompare 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 typeThe 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

"Test connection" confirms the platform can reach the source and log in. It does not read the tables the sync will read, does not check per-table grants, and does not exercise tenant scoping. Treat a green connection test as evidence about credentials and reachability only — never as proof that a later read will return the right rows to the right tenant.

404 Schema not found

You uploaded a schema, and a call against it 404s. Four causes, and the first two are told apart by whether it resolves itself.
CauseThe one check
Propagation raceIt 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 rejectedThe 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 discardedThe 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 addressThe 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?

When a write does not go through, the status code is telling you where in the pipeline it stopped — and the codes do not mean what they seem to. The rule below comes straight from how the platform orders its checks.
ResponseWhat it actually means
403A real authorization denial. This is the response that means "you are not allowed."
404Usually 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.
422Authorization 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

To know whether a failure is an authorization decision at all, run the identical request with an admin credential first. Only if the admin succeeds where the caller failed is it genuinely about permissions. This one step separates a policy problem from a scope, schema, or payload problem.

⚠️ A field-level policy strips, it does not always refuse

Field-level access control can remove a field a role is not permitted to write while allowing the rest of the write to proceed — so a create or update can succeed while quietly not saving one of the values you sent. This is deliberate (see Access Policies), and it is why the read-back habit matters: after any write, read the record and diff it against your payload. A field that came back missing or unchanged was not persisted.

A value looks wrong: $0.00, or "not configured"

Two more look-alikes, both where a display value carries less information than it appears to.

A money or metric field reads 0

A genuine zero and "the query matched nothing" render the same way. An aggregate over rows that matched a filter returns a populated set of fields; an aggregate that matched no rows returns an empty fields object, which downstream renders as 0. If a revenue tile reads $0.00, check whether the underlying aggregate matched any rows at all — a common cause is a filter on a status value the schema does not actually use, so nothing ever matches.

"Integration not configured" — but you configured it

Three causes. It is genuinely absent; or it was saved under a different scope than the one you are reading from (check that the tenant you configured it in is the tenant you are calling from); or it is configured and reports active but has never actually reached the provider. A configuration's status reflects that the required fields are present and well-formed — not that a call to the provider has ever succeeded. "Configured" and "working" are independent facts; confirm the second with a real call, not the status field.

First check by layer

When the symptom does not match one of the look-alikes above, start from the layer it is in.
LayerFirst thing to check
Auth / scopeGET /api/v1/auth/session — is the domain, project and tenant what you expect? Most "permission" problems are scope problems.
SchemaRead the upload response, not the downstream errors. Confirm the schema is addressed as namespace:type and that the namespace matches the project's.
CRUD / writeRead 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 / listFilters go through POST /query, not query-string parameters on a list. An unrecognised list parameter is dropped, and the list returns 200 unfiltered.
ConnectorThe run record (GET /api/v1/connectors/{id}/status), never the response that started the run. Look at status, row counts, and per-record errors.
WorkflowThe run's step logs. An accepted workflow that wrote no step logs did not execute its steps.
DeploymentThe 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)

Some things are genuinely not observable. Knowing which saves you the hour of trying.
  • •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

Because so many failures are silent, the reliable way to know something works is to observe the effect, not the status. Two habits pay for themselves:
  • •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.
bash
# 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

Before you file, settle one question: is the problem your call, or the platform? There is no public status page, so the practical signal is the API's own health check — GET /health on api.supero.dev. A 200 means the API and its store are up, which points the investigation back at your request (scope, schema, payload) using the checks above. If health is failing or requests are timing out broadly, it is not your call, and that belongs in the report.
When you have narrowed it down and it is genuinely a platform problem, report it. Anyone can sign up at the issue tracker to file and follow issues.

ℹ️ Where to report

File at bugs.supero.dev — signup is open, and the form captures priority, severity, category, steps, expected/actual and an environment field. For account, billing or production matters you can also reach the addresses on Contact & Links.
A report that gets resolved quickly contains what the platform can actually produce about the failure:
  • •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.
What level of support you get depends on your plan — see Plans & Billing. Response commitments apply to the higher tiers; there is no uptime SLA outside an Enterprise agreement.

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

**Further reading.** Three ways a suite stays green while proving nothing, found in ours: Your unauthenticated test client is probably authenticated.