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. |
ANONYMOUS_AUTH_DISABLED |
403 | Anonymous sign-in is off on this server. | Enable it, or sign in with a real identity. |
API_KEY_FORBIDDEN |
403 | An API key was used on a route only people may call. | Use a user session. |
API_KEY_SELF_MANAGEMENT_FORBIDDEN |
403 | An API key tried to create, list or revoke API keys. | Manage keys as a signed-in admin. |
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. | Sign in, or start a password reset. |
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. |
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 |
401 | The OTP or MFA code is wrong. | Retry with the current code. |
INVALID_CREDENTIALS |
401 | Wrong email or password — deliberately not saying which. | Retry, or reset the password. |
INVALID_TOKEN |
400 | A verification, reset or magic-link token is malformed or unknown. | Request a fresh link. |
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. |
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_EXISTS |
409 | That role name is taken. | Pick another name. |
ROLE_LOOKUP_FAILED |
503 | Roles could not be read for an admin-gated request. Fails closed rather than trusting the token’s own claim. | Retry; check the database. |
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. | Sign in again. |
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. | Request a fresh one. |
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 configured policy. | Choose a stronger one. |
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. |
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=. |
DB_PERMISSION_DENIED |
500 | Postgres refused the statement (42501): either a row-level-security policy denying this role, or a missing GRANT. |
See Troubleshooting. |
FIELD_NOT_READABLE |
400 | A filter, orderBy, fields, aggregate select or groupBy names a field this caller’s roles cannot read (access.read). A field no response can carry has to be one no query can interrogate, or the value is readable one predicate at a time. details.violations names each field. |
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. details.violations names each field. |
Remove the field, or acquire the role. A field nobody may write answers VALIDATION_EXCLUDED_FIELDS instead. |
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_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, or names them without upsert: true. Postgres would otherwise answer 42P10 from inside a transaction that has already done work. |
Declare validation: { unique: true } or a unique index; the message lists the targets that do exist. |
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. | 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_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"]}. |
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. | 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 | ?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. |
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. |
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. |
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. |
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. | 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_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_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 case-insensitive matching 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. |
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_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. details carries the property, the accepted list and the content type sent. |
Upload an accepted type, or widen acceptedFiles. |
STORAGE_NOT_CONFIGURED |
503 | No storage backend is configured on this server. | Configure S3, GCS, or local storage. |
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 | 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 — 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. |
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 says so: that is the difference between a typo and a broken deploy. | 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. |
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. | Turn it on with schemaEditor. |
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. |
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 dependency was unreachable. | 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 stated status
disagrees with the source, 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.