Error codes
Every failure a Rebase backend returns uses one envelope and carries a stable
code. The code is the thing to branch on: the message is written for a person
and may be reworded, the status is shared by a dozen different problems, and the
code is neither.
The envelope
Section titled “The envelope”{ "error": { "message": "Schema drift: table \"posts\" does not exist.", "code": "SCHEMA_DRIFT", "details": { "dbCode": "42P01" }, "requestId": "6f1b2f3e-8a0c-4d1b-9c3e-2a5b7c9d1e0f" }}message— human-readable. For a4xxit is the server’s own message; for a5xxit is deliberately generic, because the underlying text can quote a host, a role or a column name.code— one of the values below. Stable across minor versions.details— optional, and never guaranteed. See the rules below.requestId— present whenever the request passed through the request-ID middleware, which is every route underbasePath.
X-Request-ID
Section titled “X-Request-ID”Every request under basePath gets an ID: the caller’s X-Request-ID header
when it is a valid UUID v4, otherwise a fresh one. It is echoed back on the
response as X-Request-ID, included in the error envelope as requestId, and
attached to the server’s log line for that request.
That is the join key. Quote it in a bug report and an operator can find the one log line that explains the failure, carrying the reason the client was never shown.
Sending your own is how a trace survives a hop: a gateway or a job runner that forwards the header gets one ID across every service that handled the request. An invalid value is ignored rather than rejected — a malformed header from a caller is not worth failing a request over — so do not assume the ID you sent is the ID you got. Read the response header.
What is in details
Section titled “What is in details”details is diagnostic, not contractual. Three rules govern it:
- Anything a route sets explicitly is always returned. These are the caller’s own mistakes described precisely: which filter field was unknown, which relation is not writable, which value did not fit its type.
- Database diagnostics are trimmed in production. When the failure came
from Postgres,
details.dbCode— the SQLSTATE — is always present: it names the class of problem and reveals nothing about the data.dbMessage,detailandhintare added only whenNODE_ENVis notproduction, because Postgres puts row contents in them.23505reportsKey (email)=(a@b.c) already exists., which answers “is this person registered?” for any address anyone cares to try. - Never branch on
details. Branch oncode. What is underdetailsis whatever was useful to a person at that call site, and it changes.
Reading a status
Section titled “Reading a status”| Status | What it says about the request |
|---|---|
400 |
Malformed, or asking for something that does not exist in the schema. Fix the request. |
401 |
Not authenticated, or the credential expired. Sign in or refresh. |
403 |
Authenticated, and not allowed. Retrying as the same identity will not help. |
404 |
No such route, collection or row — or a row that row-level security hides. |
409 |
A conflict with existing state: a duplicate, or a concurrent write. |
413 415 422 |
The body is too large, the wrong media type, or semantically rejected. |
429 |
Rate limited. Back off; the message says for how long. |
500 |
The server or its database is wrong, not the caller. Check the logs. |
501 |
The route exists, and this deployment cannot serve it — a feature that is off or unconfigured. |
502 503 504 |
A dependency was unreachable, unconfigured, or too slow. |
Authentication and accounts
Section titled “Authentication and accounts”| Code | Status | Means | Do |
|---|---|---|---|
AAL2_REQUIRED |
403 | The route needs a second factor and the session has only one. | Complete the MFA challenge, then retry. |
ALREADY_VERIFIED |
400 | The address or factor is already verified. | Nothing — the desired state is already true. |
ACCOUNT_OUTRANKS_CALLER |
403 | The account being edited, reset or deleted holds a role or scope the caller does not. details.beyond names them. |
Have someone who holds them make the change. |
ANONYMOUS_AUTH_DISABLED |
403 | Anonymous sign-in is off on this server. | Enable it, or sign in with a real identity. |
API_KEY_SELF_MANAGEMENT_FORBIDDEN |
403 | An API key tried to list, create, change or revoke API keys, its own kind or personal ones. | Manage keys as a person holding keys:write, or with the service key. |
AUTH_MIDDLEWARE_MISSING |
500 | A guarded route ran with no Rebase auth middleware before it, so the caller’s credential was never looked at. | Mount the app through the functions router rather than onto your own server directly. |
BOOTSTRAP_ANONYMOUS |
403 | First-admin bootstrap was attempted by an anonymous caller. | Sign in first. |
BOOTSTRAP_COMPLETED |
403 | The first admin already exists. | Have an existing admin grant the role. |
BOOTSTRAP_NOT_FIRST_USER |
403 | Bootstrap is only for the very first user, and this is not it. | Have an existing admin grant the role. |
CAPTCHA_FAILED |
400 | The provider rejected the CAPTCHA token. | Solve a fresh challenge. |
CAPTCHA_REQUIRED |
400 | The route requires a CAPTCHA token and none was sent. | Include the token. |
CHALLENGE_EXHAUSTED |
401 | Too many wrong codes against one MFA challenge. | Start a new challenge. |
EMAIL_EXISTS |
409 | An account with that address already exists — on registration, on an address change, and on following an address-change link when another account took the address while it waited. | Sign in, or start a password reset; for an address change, pick another address. |
EMAIL_NOT_CONFIGURED |
503 | Magic links or OTP were requested and the server has no mail transport. | Configure SMTP, or use another sign-in method. |
EMAIL_NOT_VERIFIED |
403 | The account exists and its address is unverified. | Verify the address. |
FACTOR_NOT_VERIFIED |
400 | The MFA factor was enrolled but never confirmed. | Confirm the factor. |
IDENTITY_ALREADY_LINKED |
409 | That OAuth identity belongs to another account. | Sign in with it, or unlink it there first. |
IMPERSONATION_FORBIDDEN |
403 | A request sent x-rebase-impersonate and its caller may not run a request as another user. Only a signed-in administrator’s own session may, never an API key or the service key. |
Send it with an administrator’s session, or leave the header out. |
IMPERSONATION_INVALID |
400 | x-rebase-impersonate was sent empty. |
Send the uid of the user to act as, or leave the header out. |
IMPERSONATION_TARGET_NOT_FOUND |
404 | x-rebase-impersonate names no active user: no account has that uid, or it is disabled. |
Check the uid. |
IMPERSONATION_UNAVAILABLE |
501 | This backend’s auth cannot run a request as another user, because its AuthAdapter has no resolveUser. |
Implement resolveUser in the adapter, or leave the header out. |
IMPERSONATION_UNSUPPORTED |
400 | x-rebase-impersonate was sent to a route that cannot run as another user. Only the data API and custom functions honour it, and the realtime socket honours impersonate on AUTHENTICATE. |
Send the request without the header. |
INVALID_ACCOUNT |
400 | The account is in a state this operation cannot act on. | See the message. |
INVALID_CHALLENGE |
400 | The MFA challenge is unknown or expired. | Start a new one. |
INVALID_CODE |
400 / 401 | The OTP or MFA code is wrong: 400 from the email-code sign-in (/auth/otp/verify), 401 from an MFA enrolment or challenge. |
Retry with the current code. |
INVALID_CREDENTIALS |
401 | Wrong email or password — deliberately not saying which. | Retry, or reset the password. |
INVALID_PLATFORM_TOKEN |
401 | An rpt_ platform token was refused: a bad signature, the wrong audience, expired, or a lifetime over the ceiling. The message says which. |
Have the platform mint a fresh one — on Rebase Cloud, run the command again. |
INVALID_SCOPES |
400 | A key was asked for something that is not a scope: malformed, unknown, or with a target the scope does not take. details.problems says which and why; details.validScopes lists the scopes a key may hold. Since 0.25 It leaves out keys:read and keys:write, which no key can hold; on 0.24 it lists them. |
Use a name from the list. See Roles and scopes. |
INVALID_TOKEN |
400 / 401 | A verification, reset, magic-link or address-change token is malformed, unknown, spent or expired (400). An OAuth provider credential or a refresh token that does not verify is a 401. | Request a fresh link, or sign in again. |
KEY_MANAGEMENT_SCOPE |
400 | A key was asked for keys:read or keys:write. No key may manage keys. details.scopes names them. |
Leave them out. |
LAST_ADMIN |
403 | The change would leave the project with no admin. | Promote someone else first. |
MFA_REQUIRED |
401 | The password was right and the account has a verified second factor, so sign-in is only half done. details carries a short-lived token scoped to the MFA challenge — it is not a session. |
Open a challenge and answer it; the challenge response issues the session. |
NO_SESSION |
401 | No session cookie or refresh token was presented. Normal on a first page load. | Sign in. |
NOT_ANONYMOUS |
400 | An upgrade-from-anonymous route was called by a real account. | Nothing to upgrade. |
OAUTH_ERROR |
401 | The OAuth provider refused, or returned an error. | Retry the flow; the message carries the provider’s reason. |
PERSONAL_KEYS_DISABLED |
403 | Personal API keys are off on this backend. | Set personalKeys: true in the users collection’s auth block. |
PERSONAL_KEY_NEEDS_ACCOUNT |
403 | The service key or a guest session tried to use the personal-key routes. Neither has an account for a key to act as. | Sign in to an account, or create a service key under /api/admin/api-keys. |
PLATFORM_TOKENS_OFF |
401 | An rpt_ platform token reached a server that does not accept them: REBASE_PLATFORM_TOKEN_KEY and REBASE_PLATFORM_TOKEN_AUDIENCE are not set. |
The platform hosting the app sets both when it deploys; redeploy, or sign in to the app another way. |
RATE_LIMITED |
429 | Too many attempts from this caller. | Back off; the message says for how long. |
REDIRECT_URI_NOT_ALLOWED |
400 | The redirect target is not on the allow-list. | Add it to the provider configuration. |
REGISTRATION_DISABLED |
403 | Self-service sign-up is off. | Have an admin create the account. |
ROLE_EXCEEDS_CALLER |
403 | The roles being granted hold a scope, or admin, that the caller does not. details.beyond names them. |
Have someone who holds them grant the role. |
ROLE_EXCEEDS_CREATOR |
403 | A service key was given an RLS role its creator does not hold. Only an admin may give any role. details.roles names them. |
Drop the role, or have an admin create the key. |
ROLE_EXISTS |
409 | That role name is taken. | Pick another name. |
ROLE_LOOKUP_FAILED |
503 | The caller’s roles could not be read — on an admin route, or on a data request on a backend with config.auth. Fails closed rather than trusting the roles in the token. A token whose uid names no account, including a uid the users table cannot hold, is a 401, not this (Since 0.25; on 0.24 a uid the table cannot hold is this 503). A database that cannot be reached is DATABASE_UNAVAILABLE, not this (Since 0.25; on 0.24 it is this 503). |
Retry; check the database. |
SCOPE_EXCEEDS_CREATOR |
403 | A key was asked for a scope its creator does not hold. A key never holds more than the account that makes it. details.scopes names them. |
Drop them, or have someone who holds them create the key. |
SCOPE_MISSING |
403 | The credential does not hold the scope this route, collection, storage source or function needs. details.requiredScope names it, target included. |
For a person, a role that declares the scope. For a key, a key that holds it. See Roles and scopes. |
SCOPE_NOT_DECLARED |
500 | Since 0.25 A route guarded with requireScope names an app scope this backend does not declare, so nobody holds it, an administrator neither. A name that is not a scope at all, or a misspelt built-in one such as data:raed, never gets this far: requireScope throws where the route is registered. On 0.24 both answer every caller 403 SCOPE_MISSING. |
Declare the scope under auth.scopes on the users collection, or correct the name. See Roles and scopes. |
SELF_DELETE |
400 | An admin tried to delete their own account. | Have another admin do it. |
SESSION_REVOKED |
401 | The session was signed out elsewhere, or every session was revoked, or the account was deleted. Every door that takes a session answers it to such an access token: the data API, storage, custom functions and the admin routes (Since 0.25; on 0.24 the first three answer UNAUTHORIZED “Invalid or expired token”). The SDK refreshes, the refresh is refused, and it signs out. |
Sign in again. |
ACCOUNT_DISABLED |
401/403 | An administrator switched the account off: sign-in (403), refresh and every token it holds (401) are refused, at every door that takes a session (Since 0.25; on 0.24 the data API, storage and custom functions answer UNAUTHORIZED). |
Ask an administrator. |
EMAIL_NOT_CONFIRMED |
403 | requireEmailVerification is on and the address is not verified yet. Answered only for a correct password; a new link is mailed. |
Follow the link in the mail. |
PROOF_REQUIRED |
409 | POST /auth/verify-email: the account holds a password or identity nobody proved, and the request proves neither. The token is not spent. |
Send password to keep it, or removeUnproven: true to verify without it. |
UNDELIVERABLE_ADDRESS |
400 / 409 | A verification mail was asked for an address mail cannot reach — a guest’s, or X’s placeholder (400) — or an account asked to move onto such an address (409, POST /auth/change-email). |
Set a real address first. |
EMAIL_UNCHANGED |
400 | POST /auth/change-email named the account’s current address. |
Nothing to change. |
ANONYMOUS_USER |
403 | A guest asked to change its email address; a guest has none of its own. | Make it an account first (POST /auth/anonymous/link). |
MFA_NOT_ENROLLED |
400 | Recovery codes were asked for an account with no verified second factor. | Enrol a factor first. |
HOOK_REJECTED |
400 | A beforeUserCreate, beforeLogin, beforeUserDelete or beforeEmailChange hook threw. The message is the hook’s; a hook that chose a 4xx status answers with it. |
See the message. |
INVALID_JSON |
400 | The request body is not JSON. | Send a JSON body. |
SELF_DISABLE |
400 | An administrator tried to disable their own account. | Ask another administrator. |
NOT_SUPPORTED |
501 | This backend’s auth repository cannot do what was asked (disable accounts, change an address). | Restart the server so the column is added, or use a repository that supports it. |
UNKNOWN_SCOPE_TARGET |
400 | A scope’s target names a collection, storage source or function this backend does not serve. details lists the ones it does. |
Fix the target. |
UNVERIFIED_IDENTITIES |
409 | A magic link, email code or password reset proved an unverified account’s address, and the account carries a sign-in identity whose provider never verified that address. The auth repository cannot remove it (it has no unlinkUserIdentity), so the proof is refused rather than leave that way in on a verified account. |
Implement unlinkUserIdentity in the custom auth repository, or have an admin review the account. |
SETUP_REQUIRED |
403 | The project has no admin yet, so this route is not available. | Complete first-admin setup. |
TOKEN_ALREADY_USED |
401 | A one-time token was replayed, or a refresh token that was rotated away more than refreshTokenReuseIntervalSeconds ago. |
Request a fresh one, or sign in again. |
TOKEN_EXPIRED |
401 | The token is past its lifetime. | Request a fresh one. |
USER_NOT_FOUND |
404 | No account with that id. | Check the id. |
WEAK_PASSWORD |
400 | The password does not meet the password policy (by default 8+ characters with an upper-case letter, a lower-case letter and a digit) — on sign-up, a reset or change, and an administrator’s chosen password: POST /api/admin/users, a create on the users collection through the data API, PUT /api/admin/users/:id or POST /api/admin/users/:id/reset-password. |
Choose a stronger one. An administrator importing accounts may send skipPasswordPolicy: true on a create. |
Data, queries and writes
Section titled “Data, queries and writes”| Code | Status | Means | Do |
|---|---|---|---|
AGGREGATE_NOT_SUPPORTED |
501 | This driver cannot compute the requested aggregate. | Use a driver that can, or compute it in the client. |
BRANCHING_UNSUPPORTED |
— | A database branch was requested over the Studio websocket on the managed development database (PGlite), where a branch is the parent and nothing would be isolated. The refusal is the same one rebase db branch prints. |
Point DATABASE_URL at a Postgres of your own (rebase dev --docker starts one) and branch there. |
BATCH_TOO_LARGE |
400 | POST /api/data/_batch carries more operations than the per-batch limit (1000 by default). One batch is one transaction and holds its locks for the whole of it. |
Send it in chunks; the message names the limit and your count. |
BATCH_UNSUPPORTED |
400 | This backend’s driver cannot write across collections atomically, and a loop of single writes would be neither atomic nor one round trip. | Send the writes as separate requests, or as per-collection /bulk calls. |
BULK_TOO_LARGE |
400 | The bulk body exceeds the configured item limit. | Split the request. |
BULK_UNSUPPORTED |
400 | This collection or driver does not support bulk writes. | Write the rows one at a time. |
CALLBACK_REJECTED |
400 | A collection callback refused the write. A throw from beforeSave/beforeDelete/after* is a 400 carrying the author’s own message; a beforeDelete that returns false is a 403. details.stage names which callback, details.path the collection. |
Read the message — it was written by this project, not by Rebase. |
CALLBACK_RECURSION |
500 | Writes started by hooks nested more than 16 deep: a hook’s context.data write runs that collection’s hooks again, and nothing in the chain stops it — usually an afterSave that updates its own row unconditionally. The whole write was rolled back. The message names the hook and the collection; details.chain lists each hook → write step. |
Make the hook’s write conditional: act on status === "new" only, or skip it when values already hold what it would write. |
CURSOR_WITH_OFFSET |
400 | ?after= was combined with ?offset= or ?page=. A cursor already says where the page starts, so an offset on top of it silently skips that many rows past the cursor — a gap the caller cannot see in the response. |
Use one or the other. |
DISTINCT_NOT_APPLICABLE |
400 | ?distinct=true was combined with a search or vector query. Both attach a per-row score, so no two rows are ever equal and DISTINCT would collapse nothing — it would look like it worked and change nothing. |
Drop one of the two. |
DISTINCT_ORDER_BY_NOT_SELECTED |
400 | A distinct read is ordered by a column it does not return. A SELECT DISTINCT can only be ordered by columns in its select list, or the rows it collapses have no defined order. details.fields names them. |
Add those fields to ?fields=, or drop them from ?orderBy=. |
DATABASE_UNAVAILABLE |
503 | Since 0.25 The database could not be reached: the connection was refused or dropped, the host did not resolve, or the server is shutting down or starting. Every door answers it, sign-in, the admin routes and the realtime socket (an ERROR frame) included. Sent with Retry-After: 5. On 0.24 the same outage answers ROLE_LOOKUP_FAILED, INTERNAL_ERROR or a 500 with the code ECONNREFUSED. |
Retry; the SDK does. Check that the database is running and reachable from the server. |
DB_CONNECT_TIMEOUT |
503 | Opening a new database connection took longer than DB_POOL_CONNECT_TIMEOUT: the database is slow to accept connections, or far away. Sent with Retry-After. |
Retry; the SDK does. Check the database’s load and the network to it. |
DB_PERMISSION_DENIED |
500 | Postgres refused the statement (42501): either a row-level-security policy denying this role, or a missing GRANT. |
See Troubleshooting. |
DB_POOL_EXHAUSTED |
503 | Every pooled database connection stayed busy for DB_POOL_CONNECT_TIMEOUT, so this request never reached the database. Load, not a fault; sent with Retry-After. The server logs the remedy once a minute. |
Retry; the SDK does. Raise DB_POOL_MAX, or find the slow queries or long transactions holding connections. |
FIELD_NOT_READABLE |
400 | A filter, orderBy, fields, aggregate select or groupBy names a field this caller’s roles cannot read (access.read), a nested path or an aggregate sort names a relation they cannot read — or a field operation ($inc, $push, …) targets one, where a refused bound would reveal the stored value. A field no response can carry has to be one no query can interrogate, or the value is readable one predicate at a time. A filter through a relation (author.salary) and an aggregate sort over a relation’s field (max(staff.salary)) are judged by the target’s rule. details.violations names each field. A field hidden with excludeFromApi answers as if it did not exist: a filter, orderBy, fields, include, aggregate select or groupBy, or vector_search naming one gets exactly the answer a misspelt name gets (UNKNOWN_FILTER_FIELD, UNKNOWN_ORDER_BY_FIELD, UNKNOWN_FIELD, UNKNOWN_RELATION, UNKNOWN_AGGREGATE_FIELD or UNKNOWN_VECTOR_PROPERTY), and no unknown-field answer lists one. |
Drop the field from the query, or acquire the role. See Field access. |
FIELD_NOT_WRITABLE |
400 | The body sets a field this caller’s roles cannot write (access.write). Refused rather than dropped: a write that discards a field would report success for an edit that did not happen. The same code refuses a write through a parent that sets a relation the caller may not write, and an update that sets a softDelete field to a value — only a delete stamps it. details.violations names each field. |
Remove the field, or acquire the role. A field nobody may write answers VALIDATION_EXCLUDED_FIELDS instead. |
HISTORY_UNAVAILABLE |
503 | A write to a collection with history: true was refused because its history entry could not be recorded: the database has no rebase.entity_history table, or, for openRebase and rebase console, no rebase.record_history function. No backend with history has booted on this database. Nothing ran, callbacks included, and reads are unaffected. Thrown by openRebase, and answered by a backend created with provision: false. |
Boot the backend once with history on and provision: true (rebase dev does), then retry. |
IDEMPOTENCY_KEY_IN_PROGRESS |
409 | An earlier request with the same Idempotency-Key is still running. |
Retry once it finishes. |
IDEMPOTENCY_KEY_REUSED |
422 | The same Idempotency-Key arrived with a different body. |
Use a new key, or send the original body. |
INVALID_AGGREGATE_FUNCTION |
400 | ?select= named a function that is not count, sum, avg, min or max. |
Use one of those; the message lists them. |
INVALID_AGGREGATE_SELECT |
400 | An ?select= entry is not fn(field), or a function other than count() was given no field. |
Write sum(total), count(), avg(score). |
INVALID_AGGREGATE_WINDOW |
400 | offset, page or orderBy on an aggregate without groupBy, or a cursor on any aggregate. An ungrouped aggregate is one row, and a group is not a row a cursor can continue after. |
Add groupBy, or drop them; page groups with offset or page. See Aggregates. |
INVALID_BATCH_BODY |
400 | A /_batch operation is missing op, collection, values or id, names a collection this backend does not serve, or reuses a ref name. |
See the message; it names the operation by index. |
INVALID_BATCH_REF |
400 | A { "$ref": "<name>.<field>" } names no earlier operation, points forward, or asks for a field the referenced row does not have. Only backward references resolve. |
Name the operation with ref before referencing it. |
INVALID_BULK_BODY |
400 | The bulk body is not the expected shape. | Send the documented items array. |
INVALID_CONFLICT_TARGET |
400 | An upsert’s on_conflict / onConflict names columns carrying no uniqueness guarantee, names them without upsert: true, or is sent on a nested create, where a matched row would move under the new parent. Postgres would otherwise answer 42P10 from inside a transaction that has already done work. Also any upsert on the users (auth) collection, where a create and an update follow different rules. |
Declare validation: { unique: true } or a unique index; the message lists the targets that do exist. On the users collection, send a POST to create and a PATCH to update. |
INVALID_COUNT |
400 | ?count= is not exact or none. Refused rather than read as the default: a mistyped ?count=nnone would pay for the count it was written to skip. |
Send exact (the default) or none. See Skipping the count. |
INVALID_CURSOR |
400 | The after cursor cannot be read, or names a row id that does not fit the collection’s key columns: it was truncated, re-encoded or built by hand. |
Pass back meta.nextCursor from the previous page unchanged. See Pagination. |
INVALID_DELETED_PARAM |
400 | ?deleted= is neither include nor only. Refused rather than ignored: a mistyped ?deleted=true that quietly hid every deleted row would look like it worked and answer the opposite question. |
Send include (live and deleted) or only (deleted alone). Omit it for live rows only. |
INVALID_DISTINCT |
400 | ?distinct= is not true or false. |
Send one of those; 1 and 0 are accepted too. |
INVALID_FIELD_OPERATION |
400 | A $inc / $push / $pull / $merge was used on a property type it is not defined on, with an operand of the wrong shape, with two operators on one field, misspelled, or on a create — where there is no stored value to operate on. |
See Writing over REST; the message names the field. |
INVALID_FILTER_FIELD |
400 | The filter names a property this collection does not have. | Check the spelling against the collection. |
INVALID_FILTER_OPERATOR |
400 | The operator is not one this property type supports — for example like on a number or a date. |
See Querying data. |
INVALID_FILTER_VALUE |
400 | A filter value cannot be read as the type of the column it was compared against: ?id=eq.abc on an integer key, a label that is not in the enum, a timestamp that is not one, a number past the type’s range. details.dbCode carries the SQLSTATE. |
Send a value of the column’s type. |
INVALID_HARD_PARAM |
400 | ?hard= is not true or false. Anything else is refused rather than read as “no” — a typo that soft-deletes when the caller asked to purge leaves them believing the data is gone. |
Send true or false. |
INVALID_INCLUDE |
400 | ?include= is malformed: not a valid path list, or nested past the maximum depth. Answered at the boundary rather than escaping the driver as a 500. |
See the message; it names the offending path. |
INVALID_INPUT |
400 | The body failed validation. | See the message. |
INVALID_LIMIT |
— | A realtime subscription asked for a limit outside the allowed range. Delivered as a WebSocket ERROR frame, not an HTTP response. |
Lower the limit. |
INVALID_LOGICAL_GROUP |
400 | An ?or= / ?and= group is malformed or nested past the allowed depth. |
See the message; it shows the flattening rule. |
INVALID_OFFSET |
400 | ?offset= is not a whole number of 0 or more. |
Send a non-negative integer. |
INVALID_ORDER_BY |
400 | ?orderBy= is not field, field:desc, or a JSON array of { field, direction }. |
See the message; it shows all three spellings. |
INVALID_PAGE |
400 | ?page= is not a whole number of 1 or more. Pages are 1-based, so ?page=0 is a mistake rather than the first page. |
Send 1 or higher, or use ?offset=. |
INVALID_PARAM |
400 | A query parameter is malformed. | See the message. |
INVALID_QUERY_BODY |
400 | Since 0.25 The body of a POST …/query holds a value the URL could not have carried: a list for a parameter the URL reads once (fields, limit, …), an object for a filter, or a parameter that is also on the URL. |
Spell each value as the URL does — a string, or a list for a repeated filter; where, include, orderBy and vector take JSON. See Queries too long for a URL. |
INVALID_VECTOR |
400 | ?vector= is not a JSON array of numbers. |
Send [0.1,0.2,0.3]. |
INVALID_VECTOR_DISTANCE |
400 | ?vector_distance= is not cosine, l2 or inner_product. |
Use one of those three. |
INVALID_VECTOR_THRESHOLD |
400 | ?vector_threshold= is not a number. |
Send a number. |
INVALID_WHERE |
400 | ?where= is not a JSON object mapping fields to conditions. |
Send {"status":["==","active"]}. |
KEY_IMMUTABLE |
400 | An update names the row’s key with a value other than the one it is addressed by — in the body, or written by a beforeSave. A row cannot move to another key: everything after the write (the read-back, afterSave, history, realtime) addresses it by the key it had. Refused before any hook runs, with nothing written, on every door. details.violations names the field. |
Create the row under the new key and delete this one. Sending the key the row already has is fine. |
MISSING_AGGREGATE_SELECT |
400 | The aggregate route was called with no ?select=. |
Add one, e.g. ?select=count(). |
NO_COLLECTIONS |
404 | The project serves no collections: none declared in code, and no tables to derive them from. Since 0.25 When tables exist but row-level security is off on them, the message names them, with the SQL to enable it, and details.unprotectedTables lists them; on 0.24 it says there are no tables. |
Create tables — a migration, SQL, or a collection file plus rebase db push — and restart. |
NOT_FOUND |
404 | No row with that id in that collection — or one that row-level security hides from this caller. | Check the id, then the collection’s securityRules. |
UNKNOWN_RELATION |
400 / 404 | ?include= names something that is not a relation on the collection. The same code answers 404 when a nested URL path names one instead, e.g. /api/data/authors/1/posts where authors declares none — there the URL names nothing, so it is a not-found rather than a malformed request. |
Check the relation’s name — the message lists the ones the collection has. A back-reference has to be declared on the parent to be traversable. |
ORDER_BY_FIELD_NOT_SORTABLE |
400 | The sort names a property that is not sortable. | Sort on a column-backed property. |
PAGE_WITH_OFFSET |
400 | ?page= was combined with ?offset= — or page with offset in a find(), which the client SDK, the in-process rebase.data and the offline client refuse with the same code before any request. They describe the same window, and page used to win silently over an offset the caller then never saw applied. |
Send one of them. |
PAYLOAD_TOO_LARGE |
413 | The body exceeds the configured limit. | Send less, or raise the limit. |
READ_ONLY_TRANSACTION |
409 | An afterRead callback tried to write. A request-scoped read runs in a READ ONLY transaction, so neither the callback nor anything it calls may write. |
Move the write out of the read: a background job, or rebase.dataAsAdmin from a cron job or a custom function. |
RELATION_HAS_NO_PIVOT |
400 | The write carried a link payload, but the path does not reach its target through a manyToMany that declares through.properties — so there is no junction row to put it on. |
Declare through.properties on the relation, or drop the payload from the write. See Relations. |
RELATION_MISCONFIGURED |
500 | A relation does not resolve against the registered schema. The operation is refused rather than skipped: dropping it would report success for a write that never happened, or emptiness for rows that exist. | Run rebase schema generate if the generated schema is older than the database. |
RELATION_NOT_UNLINKABLE |
400 | The relation cannot be unlinked from this side. | Write from the owning side. |
RELATION_NOT_WRITABLE |
400 | The nested path is not a writable relation. | See Relations. |
RELATION_PIVOT_UNSUPPORTED |
400 | The relation declares junction columns, but this data source cannot write them. | The link itself still works; only the payload on it does not. Check the driver’s capabilities. |
RELATION_SOURCE_KEY_EMPTY |
400 | A relation write had no source key to hang the link on. | Save the parent row first. |
RESULT_TOO_LARGE |
— | Since 0.25 A statement sent over the realtime socket’s EXECUTE_SQL — admin.executeSql from a client, the admin panel’s catalogue reads — returned more than 128 MB of rows, so none of it was sent. Delivered as a WebSocket ERROR frame. The SQL console’s own runs are not refused: they show the first rows. |
Add a LIMIT, or select fewer columns, and read the result in parts. |
ROW_IN_TRASH |
409 | An upsert’s key belongs to a row in the trash (a soft-deleted row still holds its key). An upsert does not write into a deleted row, on any door: REST ?on_conflict=, /bulk and /_batch upserts, the SDK and the realtime socket. Nothing was written. |
Restore the row first (an update setting the soft-delete field to null), or purge it with ?hard=true and upsert again. See Soft delete. |
SCHEMA_DRIFT |
500 | A table or column the code expects does not exist in the database. | rebase db push in development; redeploy on a managed tenant. |
SCORE_CURSOR_UNSUPPORTED |
400 | startAfter was combined with orderBy: "_score". Relevance is computed per query rather than stored, so it cannot key a cursor. |
Page relevance with limit/offset, or order by a column. |
SQL_ERROR |
400 | The database refused SQL a person wrote. Over the realtime socket’s EXECUTE_SQL it is an ERROR frame carrying the database’s message. Since 0.25 POST /api/admin/sql/csv answers it before any of the file is sent: a syntax error, a write (the download runs read-only), more than one statement, or a role the connection may not take. |
Fix the SQL. A download takes one SELECT, WITH … SELECT, TABLE or VALUES. |
TENANT_IMMUTABLE |
400 | A write would move a row from one tenant to another. A row cannot change tenant. details.violations names the field. |
Create the row in the other tenant and delete this one, or write with a role in tenant.bypassRoles. |
TENANT_MISMATCH |
400 | The write names a tenant this caller does not belong to; the database would refuse it as well. | Write into a tenant the caller belongs to, or authenticate as one that belongs to it. |
TENANT_REQUIRED |
400 | The collection is tenant-scoped and the tenant cannot be inferred: the request carries none, or the caller belongs to several. | Send the tenant field explicitly, or authenticate as a caller belonging to exactly one. |
TRANSACTION_ABORTED |
500 | A statement on the write’s transaction failed and a callback caught the error. Postgres had already aborted the transaction, so the write was rolled back and nothing was stored. Only a context.data create, update or delete the database refused is undone on its own and safe to catch. |
Let the failure throw, or check before running the statement. See Transaction Semantics. |
UNKNOWN_FIELD |
400 | ?fields= names a field the collection does not have. |
Check the spelling; the message lists the valid fields. |
UNKNOWN_AGGREGATE_FIELD |
400 | An aggregate or groupBy names a field the collection does not have. |
Check the spelling; the message lists the valid fields. |
UNKNOWN_FILTER_FIELD |
400 | The filter names a field this collection — or a relation’s target — does not have, or one hidden with excludeFromApi. |
Check the spelling; the message lists the valid fields. |
UNKNOWN_FILTER_OPERATOR |
400 | The filter names an operator that does not exist. | The message lists every operator. |
UNKNOWN_MESSAGE_TYPE |
400 | A realtime socket frame has a type the server does not handle. Answered as an ERROR frame carrying the request’s requestId, so the request settles instead of timing out. |
Check the frame’s type; the message names it. |
UNKNOWN_ORDER_BY_FIELD |
400 | The sort names a field this collection does not have. | Check the spelling; the message lists the valid fields. |
PRECONDITION_FAILED |
412 | An If-Match named a version of the row that is no longer current: somebody wrote to it between the read and this write. Nothing was written. |
Re-read the row, re-apply the change, and send the new ETag. |
UNKNOWN_RESPONSE_FIELD |
400 | ?fields= asks for a field the collection does not have. |
Check the spelling; the message lists the known fields. |
UNKNOWN_VECTOR_PROPERTY |
400 | A vector search named a property that is not a vector on this collection. |
The message lists the collection’s vector properties. |
UNSUPPORTED_MEDIA_TYPE |
415 | The Content-Type is not one this route accepts. |
Send the type the route documents. |
UNSUPPORTED_OPTION |
— | Thrown, not answered: a write through the in-process accessor (context.data in a callback, rebase.data in a function or cron) was given ifMatch or idempotencyKey. That door has no precondition check or idempotency store, so the option is refused before anything is written rather than ignored. |
Drop the option there, or make the write through the HTTP client, which honours both. |
UNSUPPORTED_RELATION_FILTER |
400 | The filter crosses a via relation, whose join path is authored one way only, so there is nothing to correlate a subquery back to. |
Filter from the owning side. |
UNSUPPORTED_RELATION_FILTER_OPERATOR |
400 | The operator is not defined for that field: a relation with no column on this row is filtered by membership, and pattern matching (like, ilike and their negations) only applies to text. |
See the message; it lists what the field accepts. |
VALIDATION_CONSTRAINT |
400 | A value broke a validation rule the property declares — a length, a range, a pattern, a required field. |
See the message; it names each violation. |
VALIDATION_EXCLUDED_FIELDS |
400 | The body writes a column marked excludeFromApi, or access: { write: [] } — the same rule, two spellings. Those are the server’s to set: a password hash, a verification token. Unlike FIELD_NOT_WRITABLE this is the same answer for every caller, admin included. |
Remove the field. See Field access. |
VALIDATION_INVALID_VALUE |
400 | A value does not fit its property type. | See the message; it names the property. |
VALIDATION_UNKNOWN_FIELDS |
400 | The body names a field the collection does not have — including an id argument on a collection keyed on something else. |
Check the spelling; the message lists the known fields. |
VECTOR_CURSOR_UNSUPPORTED |
400 | A cursor (after / startAfter) was combined with a vector search. Rows are ordered by a distance computed per query, which cannot key a cursor, so a vector listing carries no nextCursor. |
Page nearest rows with limit/offset. |
WRITE_DENIED |
403 | A security rule or row-level-security policy refused the write. | Check the collection’s securityRules. |
PG_<SQLSTATE> — a constraint the database refused
Section titled “PG_<SQLSTATE> — a constraint the database refused”A write that Postgres rejects for a reason that is the caller’s data answers
with the SQLSTATE in the code: PG_23505, PG_23503, and so on. That is a
family, not a list — Postgres defines hundreds of SQLSTATEs — but only two
classes ever reach it, because only those two are the caller’s fault:
- class 23, integrity constraint violation: a duplicate, a foreign key that points at nothing, a NOT NULL column left empty;
- class 22, data exception: a value the column’s type cannot hold.
Everything else — a dropped connection, a missing column, a privilege problem —
is the server’s, and stays a 500. So code.startsWith("PG_") is a safe test
for “the row I sent was wrong”, and the four below are the ones a client
actually meets. details.dbCode carries the same SQLSTATE for all of them, and
the message names the constraint.
| Code | Status | Means | Do |
|---|---|---|---|
PG_22P02 |
400 | A value could not be read as the column’s type — the write-side twin of INVALID_FILTER_VALUE. |
Send a value of the column’s type. |
PG_23502 |
400 | A NOT NULL column was left empty. |
Send the field, or give the column a default. |
PG_23503 |
400 | A foreign key points at a row that does not exist. | Create the target row first, or fix the id. |
PG_23505 |
409 | A unique constraint was violated. The message names the constraint. | Use a different value, or update the existing row. |
Storage
Section titled “Storage”| Code | Status | Means | Do |
|---|---|---|---|
INVALID_LIST_OPTIONS |
400 | A storage listing’s maxResults is not a whole number of at least 1, or its pageToken is not one the source issued. A page size below one used to answer an empty page carrying the same token, so a while (pageToken) loop never ended. |
Send a positive maxResults, and pass back nextPageToken unchanged. |
INVALID_STORAGE_BUCKET |
400 | The bucket name is malformed. | Check the name. |
INVALID_STORAGE_KEY |
400 | The object key is malformed, or escapes its prefix. | Check the key. |
INVALID_TRANSFORM_OPTIONS |
400 | The image-transform parameters are out of range or contradictory. | See Storage. |
STORAGE_FILE_TOO_LARGE |
413 | The upload exceeds the maxSize the target property declares. Enforced on the server, not only in the browser. details carries the property, the limit and the actual size. |
Upload a smaller file, or raise maxSize on the property. |
STORAGE_FILE_TYPE_REFUSED |
400 | The upload’s type is not in the property’s acceptedFiles, or not in the storage source’s own allowedMimeTypes. details carries the property (or the source’s list), the accepted list and the content type sent. |
Upload an accepted type, or widen acceptedFiles / allowedMimeTypes. |
STORAGE_MISCONFIGURED |
502 | The S3 or GCS bucket this source is configured with does not exist, or the provider refused this deployment’s credentials for it. The message names the bucket; details.reason is NoSuchBucket or the provider’s refusal. A missing bucket used to read as “file not found” on every file. |
Fix the bucket name (S3_BUCKET / GCS_BUCKET, __<KEY> for a named source) or the credentials’ permissions. |
STORAGE_NOT_CONFIGURED |
501 / 503 | No storage backend is configured on this server, or none is the default and the request names no storage source; the message names the sources that are served (501). A resumable upload whose source disappeared before it finished is a 503. | Configure S3, GCS, or local storage, or name a source with ?storageId=. |
STORAGE_SCAN_INCOMPLETE |
409 | Since 0.25 DELETE /api/admin/storage/orphans (rebase storage orphans --delete) could not read every collection, so a file one of them names would look unused. Nothing was deleted; details.unscanned names each collection and why it could not be read. |
Fix what stops those collections from being read, and run it again. |
STORAGE_SOURCE_NOT_CONFIGURED |
501 | The storage source is declared but has no credentials here. | Set that source’s environment variables. |
STORAGE_WRITE_FAILED |
502 | The storage backend refused or dropped the write. | Check its own logs and credentials. |
TRANSFORM_OVERLOADED |
503 | Too many image transforms are in flight. | Retry; consider a CDN in front. |
UNKNOWN_STORAGE_SOURCE |
400 / 404 | The request named a storage source (?storageId=) this project does not declare. A bucket this deployment does not serve is the same code at 404 (on a listing, and on an upload, a folder or a resumable upload to S3 or GCS) — the store is what is missing, and details names the buckets and the sources that do exist. Both used to come back as “file not found”, identical to a key that is simply absent. |
Declare the source in config/resources.ts, or check GET /api/storage/sources. |
UPLOAD_LOCKED |
423 | A resumable upload’s PATCH arrived while another chunk of the same upload was still being received — almost always a client retrying the chunk in flight. It is refused rather than queued, as tusd does. |
Ask for the offset with HEAD /api/storage/tus/:id and resume from there; tus-js-client backs off and retries on its own. See Resumable uploads (TUS). |
Custom functions
Section titled “Custom functions”| Code | Status | Means | Do |
|---|---|---|---|
FUNCTION_NOT_FOUND |
404 | No function of that name is served — or one is, and its own routes do not cover the path after it. A signed-in caller is also told what is served; an anonymous one is not, because that list is an inventory of every custom endpoint. When a file of that name failed to load, the message tells a signed-in caller so, naming the file: that is the difference between a typo and a broken deploy. An anonymous caller gets the plain unknown-name answer, as for a name that matches nothing. | Check the name against GET /api/functions, or the boot log for a file that did not load. |
FUNCTION_TIMEOUT |
504 | The handler exceeded its timeout. It is still running; it cannot be cancelled from here. | Give outbound calls an AbortSignal, or raise REBASE_FUNCTIONS_TIMEOUT_MS. |
FUNCTIONS_UPSTREAM_UNREACHABLE |
502 | This process proxies functions to another one, which did not answer. | Check that the functions unit is running. |
Cron jobs
Section titled “Cron jobs”| Code | Status | Means | Do |
|---|---|---|---|
CRON_JOB_ALREADY_EXECUTING |
409 | A manual trigger of a cron job that is already running — on the process that answered, or on another one holding the job’s run lease, such as the worker behind an api process. Nothing ran. The skip is in the job’s history, and details.log is that entry; its line names the process running the job. |
Try again when that run has finished. See Cron Jobs. |
Admin surfaces and schema editing
Section titled “Admin surfaces and schema editing”These say a feature is off or unconfigured rather than that the request was
wrong. Each is also reported on the corresponding /status route with a 200,
so a panel can grey the feature out instead of showing an error.
| Code | Status | Means | Do |
|---|---|---|---|
ADMIN_SURFACE_UNAVAILABLE |
501 | An admin-only surface was called on a server with no authentication configured, so nothing can tell an admin from a stranger. | Set auth.jwtSecret, or pass an AuthAdapter. |
CONTRACT_UNAVAILABLE |
404 | The project contract is served only when authentication is configured — it describes every table and relation. | Configure auth. /meta/schema-version is always served. |
DEV_MAILBOX_UNAVAILABLE |
501 | No development mailbox is active. Mail is captured only when SMTP_HOST is unset and NODE_ENV is not production. |
Unset SMTP_HOST in development, or read the real inbox. |
INVALID_CHANGE |
400 | The proposed schema change is not well-formed. | See the message. |
SCHEMA_CHANGE_FAILED |
400 | Applying a planned schema change failed for a reason more specific codes do not cover. | See the message; it is the underlying failure verbatim. |
SCHEMA_CHANGE_UNAPPLICABLE |
400 | The change is valid and cannot be applied to the schema as it stands. | See the message. |
SCHEMA_EDIT_DIRTY_TREE |
409 | The repository has uncommitted changes, so the edit could not be applied safely. | Commit or stash, then retry. |
SCHEMA_EDIT_REFUSED |
400 | The schema editor refused the edit. | See the message; it is the editor’s own refusal. |
SCHEMA_EDIT_REQUIRES_A_PERSON |
403 | An API key or other machine principal tried to apply a schema change. | Sign in as a user, or set liveSchema.allowMachineApply. |
SCHEMA_EDITING_NO_COLLECTIONS_DIR |
501 | Live schema editing needs collectionsDir or liveSchema.repository, and this server was started with neither. |
Configure one. |
SCHEMA_EDITING_NO_REPOSITORY |
503 | Planning works; there is no repository to commit the change to. | Configure liveSchema.repository. |
SCHEMA_EDITING_UNSUPPORTED |
503 | This driver cannot plan schema changes. | Live editing is available on Postgres. |
SCHEMA_EDITOR_BAAS_MODE |
501 | Collections are introspected from the database here, so there are no source files to edit. | Change the schema with a migration. |
SCHEMA_EDITOR_DISABLED |
501 | The schema editor is turned off for this server. A built bundle always answers this. | Turn it on with schemaEditor. On a bundle, set REBASE_LIVE_SCHEMA_REPOSITORY to edit collections through a repository — see Live schema editing. |
SCHEMA_EDITOR_MISSING_DEPENDENCY |
501 | The schema editor needs ts-morph, which is not installed. |
pnpm add -D ts-morph@28.0.0. |
SCHEMA_EDITOR_NO_COLLECTIONS_DIR |
501 | The server has no collectionsDir, so the editor has nothing to write to. |
Set collectionsDir. |
SCHEMA_EDITOR_PRODUCTION |
501 | The editor is off under NODE_ENV=production: a deployed server’s files are rebuilt from your repository on every deploy, so an edit here would be discarded. |
Edit collections in development and deploy, or configure liveSchema.repository to commit them there. |
SQL_EXPORT_UNSUPPORTED |
501 | Since 0.25 POST /api/admin/sql/csv cannot stream a result here: the in-process development database (PGlite) has one connection, and a cursor held across a download would hold every other query behind it; or the data source has no SQL cursor. |
Point DATABASE_URL at a Postgres of your own (rebase dev --docker starts one), or add a LIMIT and export the rows on screen. |
Generic codes
Section titled “Generic codes”A route uses one of these when nothing more specific applies.
| Code | Status | Means | Do |
|---|---|---|---|
BAD_REQUEST |
400 | Malformed, and nothing more specific applies. | See the message. |
UNAUTHORIZED |
401 | Not authenticated, or the credential was rejected. | Sign in or refresh. |
FORBIDDEN |
403 | Authenticated, and not allowed. | Retrying as the same identity will not help. |
CONFLICT |
409 | A conflict with existing state. | See the message. |
INTERNAL_ERROR |
500 | Something on the server failed. The message is generic on purpose. | Quote the requestId; the reason is in the logs. |
NOT_CONFIGURED |
503 | A dependency this route needs is not configured on this server. | See the message. |
SERVICE_UNAVAILABLE |
503 | A service the request depends on could not be reached: an S3 or MinIO bucket, or a host a function fetch()es that refuses the connection or does not resolve. The server log names the host; the response does not. An unreachable database is DATABASE_UNAVAILABLE. Since 0.25 A connection to such a service that fails answers this; on 0.24 it answers a 500 whose code is the socket error (ECONNREFUSED), or INTERNAL_ERROR. |
Retry; check the logs. |
Keeping this page true
Section titled “Keeping this page true”pnpm verify:docs fails when a code the server can raise is missing from these
tables, when a table lists a code nothing can raise, when a row’s statuses are not
exactly the ones the source raises the code with, or when a code family like PG_<SQLSTATE> has no row
for a SQLSTATE callers meet. The stage is
tooling/scripts/docs-verify/check-error-codes.mjs.
It checks itself first. The scan reads codes out of TypeScript rather than out of
a running server, so its blind spots are silent by construction: it once could
not see a code passed through a one-line wrapper, or one written after a message
containing a ), and reported “every code the server can raise is documented”
over a page missing seventeen of them. So the stage runs a fixture of exactly
those shapes before it reads this page, and refuses to report anything if it
cannot see them.