Repository navigation
Error Codes
Every framework failure is an UltimateError carrying a stable X_* code, a concrete cause, and the exact command or edit that fixes it. Same content in the terminal, the dev overlay, the HTTP body, the MCP tool result and --json.
X_DB_DRIFT: schema differs from migrations
cause: table "posts" has column "publish_at" not present in any migration
fix: x db gen "add publish_at"
{"code":"X_DB_DRIFT","title":"schema differs from migrations","cause":"table \"posts\" has column \"publish_at\" not present in any migration","fix":"x db gen \"add publish_at\"","docs":"https://github.com/developerz-ai/ultimate/wiki/Error-Codes"}The HTTP body is the same content as an RFC 9457 problem document — type, title, status, detail, plus code, cause, fix, docs, requestId and retry (terminal, retryable or retry-after: the class a client maps to an exception, on every document, a hidden 5xx included) as top-level extension members. Two more are carried when the error has them: issues, the per-field rejections of a validation failure, and meta, the keys of the error's own meta that the app declared for that code with registerProblemMeta (everything else in meta is operator-only and never leaves the process). Both are all-or-nothing and both are dropped, with cause, on a 5xx nobody classified.
| Rule | Detail |
|---|---|
| Lookup |
x errors explain <CODE> --json, or the MCP errors.explain tool — same content as this page |
| Uniqueness | one code is owned by exactly one package; a collision throws X_ERROR_CODE_DUPLICATE at registration |
| Stability | codes never change meaning. A renamed concept gets a new code and the old one stays documented |
fix |
always runnable or editable as written. If a fix reads "do the right thing", that is a bug — file it |
Bare Error
|
never thrown by the framework. Anything caught is normalised through toUltimateError()
|
| Registration | a code exists when its owning package calls registerErrorCodes(). That one call is what makes it explainable, unique and documented-or-fail — a code emitted as a Finding rather than thrown is registered the same way |
| App codes | an app declares each of its own codes ONCE: defineError({ code, title, status, cause, fix }) from @ultimat3/core/page (offSocket: true in place of status for a code no response carries) registers the title, the HTTP status and the retry, and returns the class to throw — throw new PostNotFound(ref), PostNotFound.is(error). A code with no status answers 500 and hides its cause, so the call makes you choose. An app code needs no row on this page; guards/error-declared.ts refuses a handwritten UltimateError subclass, a literal-coded new UltimateError and a defineError imported from the @ultimat3/core barrel, which carries the framework's titles tables into every island that throws (12,019 B against 6,507 B, one declaration thrown). A framework code goes through bun run new-error-code instead |
| Enforcement | the errors step of x verify fails on an empty or advice-only fix, or one that runs the command before the edit it needs (X_ERROR_FIX_INVALID), on a fix citing a file the repo does not have (X_ERROR_FIX_PATH_MISSING), on a declared code with no row on this page (X_ERROR_CODE_UNDOCUMENTED), on a row this page presents as live that no package registers (X_ERROR_CODE_UNREGISTERED), and on a code: reached through a name nothing in its own file declares (X_ERROR_CODE_UNRESOLVED) |
As of 2026-10 every code above Reserved codes resolves through x errors explain, with one exception the gate knows about: this repository's own gate scripts (X_BOUNDARY_VIOLATION, X_ROADMAP_*, X_REFERENCE_APP_*, X_TRUST_*, X_CATALOG_CARRIER_UNSCANNED, X_CATCH_RENDER_PIN_STALE, X_CATCH_RENDER_UNSAFE, X_CATCH_RENDER_UNSCANNED, X_CONFIG_KEY_UNREAD, X_CONFIG_READERS_UNSCANNED, X_CONFIG_READER_PIN_STALE, X_DECLARED_KEY_UNREAD, X_DECLARATION_READER_PIN_STALE, X_DECLARATION_READERS_UNSCANNED, X_DOC_COMMAND_PIN_EXCEEDED, X_DOC_CONFIG_KEY_UNKNOWN, X_DOC_CONFIG_ALLOWANCE_STALE, X_SCAFFOLD_VERIFY_UNREADABLE, X_TIER_FLOOR_STALE, X_TIER_FLOOR_UNDECLARED, X_SCAFFOLD_OVERRIDES_EMPTY, X_SCAFFOLD_GATE_RED, X_SCAFFOLD_FIRST_RUN_FAILED, X_SETUP_INSTALL_FAILED, X_ADMIN_FLATTENER_VIOLATION, X_CONFIG_IMPORT_OUTSIDE_LOADER, X_ERROR_RENDER_UNSAFE, X_ERROR_STATUS_MISSING, X_ERROR_STATUS_BACKLOG_STALE, X_ERROR_STATUS_UNKNOWN_CODE, X_CATALOG_KEY_UNREACHABLE, X_PUBLISH_LIST_INCOMPLETE, X_PUBLISH_LIST_UNKNOWN, X_BENCH_CLAIM_STALE, X_BENCH_URL_REQUIRED, X_BENCH_STREAM_CLOSED, X_BENCH_RUN_TOO_SHORT, X_BENCH_RUN_FAILED, X_WIKI_TABLE_MALFORMED, X_WIKI_TABLE_UNSCANNED, X_FRAME_DOCS_STALE, X_CHART_VERSION_STALE, X_IMAGE_LIBC_MISMATCH, X_IMAGE_GUARD_MISSING, X_IMAGE_SECRET_UNIGNORED, X_IMAGE_CONTENT_UNIGNORED, X_DOC_FIX_UNRUNNABLE, X_DOC_FIX_UNSCANNED, X_DOC_FIX_DISAGREES, X_DOC_FIX_DISAGREES_PIN_STALE, X_DOC_CAUSE_ECHOES_TITLE, X_DOC_COMMAND_UNKNOWN, X_DOC_COMMAND_ALLOWANCE_STALE, X_DOC_COMMAND_UNSCANNED, X_DOC_COMMAND_PIN_STALE, X_DOC_TEST_PATH_BARE, X_DOC_TEST_PATH_UNSCANNED, X_TSCONFIG_PATH_MISSING, X_TSCONFIG_PATH_STALE, X_TSCONFIG_PATHS_UNSCANNED, X_DOC_GATE_STEPS_STALE, X_DOC_GATE_STEPS_UNSCANNED, X_DOC_SETUP_COMMAND_STALE, X_DOC_SETUP_COMMANDS_UNSCANNED, X_DOC_SETUP_COMMAND_STALE, X_DOC_SETUP_COMMANDS_UNSCANNED, X_README_EXAMPLE_UNCOMPILED, X_README_EXAMPLE_PIN_STALE, X_README_EXAMPLE_UNSCANNED, X_README_SAMPLE_DRIFT, X_README_FACTS_DRIFT, X_PRODUCTION_APPS_DRIFT, X_ADOPTION_CLAIM_UNGENERATED, X_PACKAGE_README_SHAPE, X_PACKAGE_KEYWORDS_MISSING, X_VERSION_STAMP_STALE, X_VERSION_STAMP_DUPLICATE, X_VERSION_LOCKSTEP_BROKEN, X_VERSION_STAMP_UNSCANNED, X_COVERAGE_BELOW, X_COVERAGE_PIN_STALE, X_COVERAGE_UNMEASURED, X_TEST_TYPECHECK_REGRESSED, X_TEST_TYPECHECK_PIN_STALE, X_TEST_TYPECHECK_UNSCANNED, X_DOC_FILE_COUNT_STALE, X_DOC_FILE_COUNT_UNSCANNED, X_DOC_RELEASE_FACT_STALE, X_DOC_RELEASE_FACT_UNSCANNED, X_DOC_PACKAGE_GRAPH_STALE, X_DOC_PACKAGE_GRAPH_UNSCANNED, X_TEST_FIX_UNRUNNABLE, X_TEST_FIX_PIN_STALE, X_TEST_FIX_UNSCANNED, X_TEST_THROW_NOT_THROWN, X_TEST_BARE_ERROR, X_TEST_BARE_ERROR_PIN_STALE, X_TEST_BARE_ERROR_UNSCANNED, X_SOURCE_BARE_ERROR, X_SOURCE_BARE_ERROR_EXEMPT_STALE, X_TEST_REAL_SLEEP, X_TEST_REAL_SLEEP_PIN_STALE, X_TEST_REAL_SLEEP_UNSCANNED, X_RELEASE_VERSION_UNSTATED, X_RELEASE_FLAG_UNKNOWN, X_RELEASE_TREE_DIRTY, X_RELEASE_BRANCH_NAME, X_RELEASE_VERSION_INVALID, X_RELEASE_MAJOR_WINDOW, X_BREAKING_ON_MAIN, X_RELEASE_UNDEPRECATED_REMOVAL, X_CHANGELOG_HIGHLIGHTS_MISSING, X_DOC_CHANGELOG_SECTION_INVALID, X_DOC_CHANGELOG_UNRELEASED_BREAKING, X_DOC_MIGRATION_COUNT_STALE, X_DOC_MIGRATION_SECTION_MISSING, X_DOC_MIGRATION_UNSCANNED, X_DOC_MIGRATION_UNPAIRED, X_DOC_MIGRATION_RANGE_STALE, X_DOC_GUARD_REFRESH_UNSTATED, X_RELEASE_UNRELEASED_MISSING, X_RELEASE_TAG_UNREACHABLE, X_RELEASE_WINDOW_UNREADABLE, X_RELEASE_HEAD_NOT_MAIN, X_HEALTH_ROW_UNREADABLE, X_LOCKFILE_STALE, X_REGISTRY_BOOTSTRAP_OWED, X_REGISTRY_LATEST_DRIFT, X_REGISTRY_UNATTESTED, X_REGISTRY_UNREACHABLE, X_REGISTRY_VERSION_BEHIND, X_GATE_CODE_UNDOCUMENTED, X_GATE_CODE_BACKLOG_STALE, X_SIDE_EFFECTS_*, X_DEAD_DOCS_HOST, X_DEAD_DOCS_HOST_PIN_STALE, X_DEAD_DOCS_HOST_UNSCANNED, X_SECRET_COMPARED_UNSAFELY, X_SECRET_COMPARE_PIN_STALE, X_SECRET_COMPARE_UNSCANNED, X_VERSION_STAMP_PIN_STALE, X_WORKSPACE_MANIFEST_UNREADABLE, X_CONFIG_KEY_READER_AMBIGUOUS, X_PROTO_CHAIN_INDEX, X_PROTO_CHAIN_INDEX_PIN_STALE, X_PROTO_CHAIN_INDEX_UNSCANNED, X_NODE_IMPORT_UNEXPLAINED, X_NODE_IMPORT_PIN_STALE, X_NODE_IMPORT_UNSCANNED, X_FLIGHT_RANDOM_UNINJECTED, X_FLIGHT_SECOND_CURVE, X_HELPER_COPY, X_NOT_IMPLEMENTED_COPY, X_NOT_IMPLEMENTED_COPY_UNSCANNED, X_FRAMEWORK_TABLE_UNAPPLIED, X_FRAMEWORK_TABLE_UNSCANNED, X_SQL_LITERAL_COPY, X_SQL_LITERAL_UNSCANNED, X_INDEX_ORDER_UNGUARDED, X_INDEX_ORDER_PIN_STALE, X_INDEX_ORDER_PIN_UNEXPLAINED, X_INDEX_ORDER_UNSCANNED, X_SKIP_CLEANUP_UNREACHED, X_SKIP_CLEANUP_UNSCANNED, X_TEST_DISCOVERY_UNSEEN, X_TEST_DISCOVERY_UNSCANNED, X_PROBE_DATABASE_FIXED, X_SPAWN_UNTIMED, X_SPAWN_TIMEOUT_PIN_STALE, X_SPAWN_TIMEOUT_PIN_UNEXPLAINED, X_SPAWN_TIMED_OUT, X_FETCH_UNBOUNDED, X_FETCH_DEADLINE_PIN_STALE, X_FETCH_DEADLINE_PIN_UNEXPLAINED, X_FINITE_BOUND_UNCHECKED, X_FINITE_BOUND_PIN_STALE, X_FINITE_BOUND_UNSCANNED, X_VOCABULARY_REDECLARED, X_VOCABULARY_UNSCANNED, X_FROZEN_RECORD_INFERRED, X_FROZEN_RECORD_UNSCANNED, X_CONFIG_READER_PIN_UNEXPLAINED, X_SECRET_COMPARE_PIN_UNEXPLAINED, X_PROTO_CHAIN_INDEX_PIN_UNEXPLAINED, X_FIX_SHELL_ARG_UNSCREENED, X_FIX_SHELL_ARG_PIN_STALE, X_FIX_SHELL_ARG_PIN_UNEXPLAINED, X_FIX_SHELL_ARG_UNSCANNED, X_BROWSER_TRANSPORT_UNSCANNED, X_CHANNEL_LITERAL, X_CHANNEL_LITERAL_UNSCANNED, X_SEAL_CALL_OUTSIDE_CORE, X_SEAL_CALL_UNSCANNED, X_KEYED_HASHER, X_KEYED_HASHER_EXEMPT_STALE, X_INSTRUMENT_SLOT_FORBIDDEN, X_INSTRUMENT_SLOT_ALLOWANCE_STALE, X_ADAPTER_PORT_INCOMPLETE, X_CLI_OUTBOUND_UNDECLARED, X_CLI_OUTBOUND_ALLOWANCE_STALE, X_PRIVATE_NAME, X_PRIVATE_NAMES_UNSET, X_GUARD_LOAD_FAILED, X_BUDGET_RAISE_UNSTATED, X_NEW_ERROR_CODE_INVALID, X_NEW_ERROR_CODE_EXISTS, X_NEW_ERROR_CODE_PATTERN_UNKNOWN, X_BUDGET_BASE_MISSING, X_PIN_RAISE_UNSTATED, X_PIN_BASE_MISSING, X_CORPUS_UNSCANNED, X_DOC_UNRELEASED_STALE, X_DOC_UNRELEASED_PIN_STALE, X_LLMS_TXT_DRIFT, X_GUARDS_DOC_DRIFT, X_GUARDS_DOC_UNSCANNED, X_CLAUDE_MD_OVERSIZE, X_CLAUDE_MD_UNSCANNED, X_CONFIG_READER_APP_UNREAD, X_FIX_PROSE, X_FIX_PROSE_PIN_STALE, X_FIX_PROSE_PIN_UNEXPLAINED, X_FIX_PROSE_UNSCANNED, X_DOC_PATH_DEAD, X_ROOT_BIN_REINTRODUCED, X_VERIFY_PART_UNNAMED, X_PLAN_STATUS_INVALID, X_PLAN_STATUS_UNSCANNED, X_URL_PATHNAME_AS_PATH, X_RELATIVE_PATH_NOT_POSIX, X_SET_COOKIE_HAND_BUILT, X_SET_COOKIE_PIN_STALE, X_SET_COOKIE_UNSCANNED, X_FACTORY_NAME_SPELLING, X_CORE_THROWER_UNCALLED, X_CORE_THROWER_PIN_STALE, X_CORE_THROWERS_UNSCANNED, X_SQL_EXPORT_UNREAD, X_SQL_EXPORT_UNSCANNED, X_WIKI_EXAMPLE_UNCOMPILED, X_WIKI_EXAMPLE_PIN_STALE, X_WIKI_EXAMPLE_UNSCANNED, X_WIKI_ANCHOR_DEAD, X_WIKI_ANCHOR_UNSCANNED, X_KNOWN_GAP_LINK_MALFORMED, X_KNOWN_GAP_UNSCANNED, X_KNOWN_GAP_UNPINNED, X_KNOWN_GAP_PIN_STALE, X_DOC_TUTORIAL_DIVERGED, X_DOC_UID_STALE, X_DOC_UID_UNSCANNED, X_DOC_BOUNDARY_RULES_STALE, X_DOC_BOUNDARY_RULES_UNSCANNED, X_SOURCE_HEADER_MISSING, X_SOURCE_HEADER_OUT_OF_CONTRACT, X_SOURCE_HEADER_PIN_STALE, X_SOURCE_HEADER_UNSCANNED, X_DEPENDENCY_NOT_ALLOWLISTED, X_DEPENDENCY_ALLOWLIST_STALE, X_DEPENDENCY_UNSCANNED, X_DEPENDENCY_RANGE_CONFLICT) never ship, so no package may own them. See Troubleshooting for symptom-first triage and CLI reference for the commands named in the fixes.
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_ABORTED |
operation aborted | the AbortSignal on ctx fired: client disconnected, or a deadline passed — including a job past its timeout, whose steps are refused from that point on |
add throwIfAborted(ctx) before expensive work, and make each vendor call with outbound({ url, vendor, dial, deadlineMs }) from @ultimat3/core/outbound, which hears ctx.signal — unwind at the deadline instead of running past it |
X_TIMEOUT |
operation exceeded its deadline | a request that ran past the configured requestTimeoutMs (30s), or past the shorter budget its caller sent as x-request-timeout-ms. The abort fires first for cooperative code, so this is what the socket gets when the handler never looked at ctx.signal. The same object is rethrown by a write attempted past the deadline: a transaction rolls back, an entity write and an enqueue refuse, so an unkeyed retry does not duplicate it. Core owns the code and classifies it retryable; @ultimat3/http throws it and maps it to 504 |
make every vendor call with outbound({ url, vendor, dial, deadlineMs }) from @ultimat3/core/outbound, which hears ctx.signal and the deadline, and call throwIfAborted(ctx) before expensive work, or call configureHttp({ requestTimeoutMs: 60_000 }) at module scope in a file under apps/*/. A write past the deadline is refused: wrap work that must land whole, runToCompletion(() => withTransaction(fn)), never a statement inside the transaction, whose COMMIT stays refused |
X_INTERNAL |
unexpected internal framework error | a non-Ultimate error escaped into framework code | fix the failure named in cause, then re-run |
X_INVARIANT |
invariant violated | framework state that "cannot happen" happened | report it with x doctor --json output attached |
X_UNREACHABLE |
unreachable branch was reached | an exhaustive switch met a value the type says cannot exist |
narrow the union at the call site |
X_NOT_IMPLEMENTED |
this driver does not implement the requested feature | an interface-complete driver whose remote half is unwritten | use the default driver, or implement the named method |
X_ASYNC_CONTEXT_UNAVAILABLE |
async context unavailable |
node:async_hooks is stubbed to {} in this runtime, so nothing can be made ambient — a browser bundle |
the scope is server-only: open it in apps/web/server.ts or a route handler, and keep every module that opens one out of the import graph of a client island entry |
X_NO_CONTEXT |
no request context is active | framework code called outside the ALS context | runWithContext(ctxOf({ … }), fn) |
X_SERVICE_MISSING |
service is not registered on the request context |
ctx.<service> used without providing it |
pass it in ctxOf({ services: { … } })
|
X_SERVICE_DUPLICATE |
a service name is registered twice | two defineService('name', ...) calls used the same name |
rename one of the two declarations |
X_ROLE_INVALID |
ROLE is not a known runtime role |
typo or an old role name in the env | set ROLE to web, sync, worker, scheduler, migrate or replicator
|
X_DRAINING |
process is draining and refuses new work | work arrived after SIGTERM | retry against another replica; the LB should already have removed this one |
X_SHUTDOWN_TIMEOUT |
graceful shutdown exceeded its deadline | an in-flight handler outlived DRAIN_TIMEOUT
|
raise configureLifecycle({ deadlineMs }) or shorten the slow handler |
X_FLIGHT_GATE_OVERLOADED |
a concurrency gate is at its ceiling and its queue is full | more callers asked for a bounded resource than flightGate({ maxConcurrent, maxQueued }) admits — argon2 hashes, a browser pool, any work whose cost is memory rather than CPU time. Past the queue bound the answer is a refusal and never a longer queue: an unbounded queue turns a load spike into a memory fault and answers it minutes late. meta carries active, queued, both bounds and retryAfterSeconds, so it is classified retry-after and @ultimat3/http renders the header from it |
retry after the Retry-After header, or widen the ceiling at the flightGate({ maxConcurrent, maxQueued }) call site — only if the box has the capacity the extra slots buy |
X_SUPERSEDED |
a later generation superseded this work | an answer came back for a generation the fence has already moved past — a reload, a resubscribe or a re-render whose predecessor finished late. Classified terminal, and declared rather than defaulted: re-running superseded work produces this same refusal by construction, so an unclassified reading would spend a job's whole retry policy proving that the world has still moved on |
discard this answer and re-issue the work against fence.generation() — whoever called bump() has already started the replacement |
X_CLIENT_TRANSPORT_FAILED |
a browser request got no answer from the app |
clientTransport got no usable answer. Three ways: the network refused or dropped the request (fetch rejected with a TypeError); a non-2xx body named no framework code, so a proxy or gateway answered instead of the app; or a 2xx body was not JSON. meta.failure says which: 'network' (no response at all), 'status' (a non-2xx no framework code explained) or 'body' (a 2xx that was not JSON; the write may have landed). That is TransportFailure in @ultimat3/core. A read, and a write carrying an idempotencyKey, are classified retryable. A write with no key is not, because it may already have landed. A non-2xx is retryable only when its status is |
x doctor --json checks the network and the gateway in front of the app, then retry. Give a write an idempotencyKey before you retry it |
X_CLIENT_RECORD_ENVELOPE_INVALID |
a response marked as a records envelope has the wrong shape | a response carried x-ultimate-records: 1, and its body is not { data, records?, removed? }: no own data, records not record type → record key → row, or removed not record type → list of keys. The cause names the position and never echoes the body |
build the body with encodeRecordEnvelope() from @ultimat3/core in the handler that set the header, or stop setting the header |
X_CLIENT_SCOPE_CHANGED |
the page changed principal while this request was in flight |
rescope() moved the page to another principal while a read was in flight. The read's answer belongs to the previous principal, so it is aborted and nothing from it is adopted. A write in flight is never refused with this: it finishes, and its records are dropped. Classified terminal. isSuperseded(error) answers true
|
discard the answer and render nothing for it: if (isSuperseded(error)) return; with isSuperseded from @ultimat3/core, then read again under the new principal |
X_READINESS_CHECK_DUPLICATE |
a readiness check name is registered twice | two registerReadinessCheck('<name>', check) calls under one name. A silent replacement leaves the probe an operator believes is running unreachable, and /readyz green on the strength of the one that replaced it — the cause lists every name already registered |
name the second check for what it actually probes, e.g. registerReadinessCheck('<name>-replica', check) — or hold the unregister the first registration returned and call it first |
X_LIFECYCLE_DRAINED |
a drained process cannot become ready again | a role called markReady() in a process whose lifecycle has already drained — a second httpServer().start(), or a boot that started a role after SIGTERM arrived. state never leaves stopped and drain() memoizes, so the socket such a role binds answers 503 to every request and nothing is left to close it: measured, still accepting connections after its own stop() returned. A container role (runRole) whose boot meets it because SIGTERM arrived mid-boot does NOT surface it: that is a rollout superseding the pod, so the boot finishes the drain, logs ultimate boot interrupted by shutdown at info with this code, and exits 0 (As of 2026-10-17) |
start every role BEFORE the first drain — a drained process is on its way out and is replaced, never reused. In a test, call resetLifecycle() from @ultimat3/core between the drain and the next start()
|
X_ERROR_RETRY_INVALID |
error retry classification is unknown or already claimed |
registerErrorRetry() given a kind that is not terminal, retryable or retry-after, or a second, different classification for one code. Core's own codes are closed to reclassification: an app that could call X_DRAINING terminal would stop the retry a rolling restart depends on |
x errors list --json — then registerErrorRetry({ <CODE>: 'retryable' }) with one of terminal, retryable, retry-after
|
X_ID_INVALID |
value is not a valid id | a hand-built string passed where a typed id is required | generate ids with uuidV7() / typedId<'post'>() from @ultimat3/core
|
X_CURSOR_INVALID |
pagination cursor is malformed, tampered with or from another query | signature mismatch — an edited cursor, or ULTIMATE_CURSOR_SECRET rotated — or a cursor built for a different query, filter or sort order |
drop the cursor and request the first page (after: null) |
X_CURSOR_SECRET_DEV |
cursors are signed with the shipped development key |
ULTIMATE_CURSOR_SECRET is unset — or set to the EMPTY string, which counts as unset and never as an empty HMAC key — outside development/test, so the signing key is the constant published in @ultimat3/core and a client can forge a page position. A process naming NO environment (ULTIMATE_ENV/NODE_ENV both unset) counts as production. Reported by x doctor as a warning, and thrown at boot by assertNoDevSecretsOutsideLocal()
|
openssl rand -hex 32 # put the output in this deploy's secret store as ULTIMATE_CURSOR_SECRET; this machine: ULTIMATE_ENV=development x env check |
X_ERROR_CODE_DUPLICATE |
error code registered twice | two packages declared the same code | rename the colliding code in the registering package's src/errors.ts
|
X_ERROR_REPORTER_DSN_INVALID |
the error monitor DSN is malformed |
SENTRY_DSN is not a URL, carries no public key, names no project, or uses a scheme that is not http/https. Thrown at boot rather than at the first failure, because a monitor that was never connected looks exactly like an app that never failed |
set SENTRY_DSN=https://<publicKey>@<host>/<projectId> in .env, then run x env check
|
X_REGISTRAR_MISSING |
no registrar is loaded for a primitive kind | the owning package is absent from the graph, so nothing announced a registrar — defineApi({ queries }) without @ultimat3/query. meta.kind names the kind, and a kind is not a package name: action and mutator → @ultimat3/action, job and task → @ultimat3/jobs, route → @ultimat3/render, entity, policy, query → their own. Importing the owner is what announces |
bun add <owning package> — e.g. bun add @ultimat3/jobs for a missing task registrar; the error's own fix: names it |
X_REGISTRAR_CONFLICT |
two different registrars are loaded for one primitive kind | two copies of the kind's owning package in the dependency tree (task → @ultimat3/jobs, mutator → @ultimat3/action, route → @ultimat3/render), each with its own registry, so half the primitives register where nothing reads them. bun pm why <owning package> names the dependents when ranges genuinely disagree |
bun update <owning package> — the error's own fix: names it |
X_SECRETS_KEY_ACL_FAILED |
key file ACL not owner-only | on Windows, icacls refused to cut the new key file ACL to the current account, so no key was written |
where.exe icacls — then whoami; or keep no key file and set ULTIMATE_SECRETS_KEY |
X_COOKIE_INVALID |
a Set-Cookie value would be dropped, misread or injectable | serializeSetCookie() was given a name, value or attribute a browser rejects or that could split the header |
serializeSetCookie(name, value, options) — correct the field named in meta.field, as the cause says |
X_OUTBOUND_TIMEOUT |
an outbound HTTP call passed its deadline or went idle | outbound() waited past the call's deadlineMs (retries and waits included), or a response body sent nothing for idleMs. The vendor or the network is slow, or the bound is tighter than the vendor's real latency. Retryable: nothing about the request was wrong |
outbound({ url, vendor, dial, deadlineMs: 30_000, idleMs: 60_000 }) — raise the bound on the call that names meta.vendor
|
X_OUTBOUND_UNREACHABLE |
an outbound HTTP call got no response | outbound() could not resolve the host, connect, finish TLS, or the connection was reset before a response arrived. The vendor is down, or an egress rule drops the traffic. Retryable |
curl -sS -m 5 -o /dev/null <meta.origin> — reachable from here? Else check the egress rule and the vendor's status, then re-run the call |
X_OUTBOUND_THROTTLED |
an outbound HTTP call was told to wait | the vendor answered a retryable status (429, 503) with a Retry-After header, and outbound()'s attempts or deadline ran out before the stated delay. meta.retryAfterSeconds carries the delay | wait meta.retryAfterSeconds before calling meta.vendor again, or lower the call rate at the site that calls outbound()
|
X_OUTBOUND_UNAVAILABLE |
an outbound HTTP call answered a retryable status | the vendor answered 5xx, 408, 409, 425 or 429 with no stated delay, on every attempt outbound() made. The vendor's fault, not the request's: meta.status names the status |
curl -sS -m 5 -o /dev/null -w '%{http_code}\n' <meta.origin> — the vendor's fault: re-run the call; if meta.status persists, check the status page of the vendor meta.vendor names |
X_OUTBOUND_REFUSED |
an outbound HTTP call answered a non-retryable status | the vendor answered a non-2xx status that the same request will get again: 400, 401, 403, 404, 422. meta.status names it and meta.detail carries the first 200 characters of the body, redacted. A 401 is a credential to refresh: the app refreshes and calls again | fix the request or the credential meta.vendor was called with, reading meta.status and meta.detail; on a 401 refresh the token, then call outbound() again |
X_OUTBOUND_TOO_LARGE |
an outbound HTTP response body passed its byte cap | the response body passed outbound()'s maxBytes (default 16 MiB): the read was cancelled at the cap and nothing past it was held |
outbound({ url, vendor, dial, deadlineMs, maxBytes: 64 * 1024 * 1024 }) to raise the cap, or read: 'stream' to consume the body in chunks |
X_OUTBOUND_HOST_BLOCKED |
an outbound HTTP call refused the address it would reach | dial: 'public' refused the URL: not https outside a local environment, credentials in the URL, a host that resolves to a loopback, private, link-local or reserved address, or a redirect hop that does | use a public https:// URL for meta.vendor, or dial: 'named' for a host the app itself names in code |
X_OUTBOUND_REDIRECTED |
an outbound HTTP call answered a redirect | the vendor answered 3xx and outbound() runs with redirect: 'error' by default, so a hop is never followed silently. meta.location names where it pointed | call the final URL meta.location names, or pass redirect: 'follow' to outbound()
|
X_OUTBOUND_RETRY_UNSAFE |
a retry was asked for on a write that is not idempotent | outbound() was given retry on a POST or PATCH without idempotent: true. A replayed write is a second charge, so the call is refused before anything is sent | add idempotent: true to the outbound() call with the vendor's idempotency-key header, or drop retry
|
X_OUTBOUND_BODY_INVALID |
an outbound HTTP response did not match its schema | json(schema) on an outbound() answer refused the body: the vendor changed its shape, or the schema is wrong. meta.issues names the rejected paths | edit the schema passed to answer.json(schema) for meta.vendor, or ask the vendor whether the response changed |
X_BREAKER_OPEN |
a circuit breaker is open and refused the call | the dependency meta.name names failed failures times in a row, so the breaker refuses calls until cooldownMs passes and one probe succeeds. meta.retryAfterSeconds is the time left | wait meta.retryAfterSeconds; check the dependency meta.name names; raise failures in breaker({ name, failures, cooldownMs }) or adjust its classify if this outage is not one |
X_ERROR_DECLARATION_INVALID |
a defineError declaration is malformed |
defineError() given both or neither of status and offSocket, a status outside 100-599, or a code that is not X_ followed by SCREAMING_SNAKE words |
edit the defineError call the cause names, then x verify --only errors |
X_CLIENT_ERROR |
a browser reported an uncaught failure | an app/ page threw in the browser: a window error, an unhandled rejection or an island whose mount() rejected — the report's cause is the browser's own message, capped, and meta.clientBuild is the build the tab was rendered by | x shot <route> --json |
X_HTTP_PATH_INPUT_CONFLICT |
a path parameter and the request body or query name one key with two values | a route pinned with a :param template (http: { path }) received the same key in the body or query string with a different value than the URL carries | send the key once: omit it from the body or query string, the URL already carries it (POST /orders/123/refund with body {}) — or send the identical value in both |
X_ERROR_UNDECLARED |
a handler threw a 4xx code its errors: list does not declare | an action or query declares errors: and its handler threw a refusal outside that list, in a test or development run or in the contract scan of the handler source | add '<CODE>' to errors: in the declaration the cause names, or stop throwing it there, then: x verify --only contract |
X_ACTION_ERRORS_INVALID |
an errors: list names a code that cannot be declared there | an action or query lists a code in errors: that is not X_SCREAMING_SNAKE, has no registered 4xx status, or is listed twice | declare the code with defineError({ code, title, status, cause, fix }) first (a 4xx status), then list it once; x errors explain <CODE> --json |
X_CONTRACT_EXERCISE_ENVIRONMENT |
a contract exercise was asked to run in a deployed environment | exerciseAction/exerciseQuery ran with ULTIMATE_ENV or NODE_ENV set to staging or production | run the .contract() test under bun test (NODE_ENV=test) |
X_CONTRACT_EXERCISE_UNSCOPED |
a contract exercise could not roll back its writes | no database client installed and no transaction open, outside the test environment | setDbClient(<test client>) before .contract(), or run under bun test |
X_IMAGE_RASTER_BUDGET |
a synchronous raster transform is over the web role pixel budget | a crop or composite asked of the web role where no raster Worker could start (a compiled executable, or a Worker that never loaded), whose inline inflate + deflate would touch more than RASTER_SYNC_PIXELS (409,600) pixels — at the measured worst rate, more than 50 ms of a blocked event loop | warm the variants off the request path: call warmVariants({ disk, key, widths, formats }) from @ultimat3/storage (packages/storage/src/image-warm.ts) in a job on the worker role, so the media route answers from storage; or run the role from source so transformImageBytes reaches its Worker |
Counter, gauge and histogram live in @ultimat3/core (metrics.ts), aggregate in process, and are read through one collectMetrics(). The wire format is a driver: MetricExporter is the seam, the default is a no-op, and metricsText() renders the Prometheus/OpenMetrics scrape body behind METRICS_PATH. An OTLP client ships As of 2026-08: otlpMetricExporter() and otlpSpanExporter() speak OTLP/HTTP JSON and read OTEL_EXPORTER_OTLP_ENDPOINT, with no new dependency. gRPC on :4317 is refused by design; scraping /metrics with an agent stays equally supported.
The three series docker/helm autoscales on are http_requests_total (the counter rps is derived from), connections and queue_demand — SCALING_METRICS in runtime-metrics.ts maps each ScalingSignal to its series, so the role table and the chart cannot drift apart.
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_METRIC_NAME_INVALID |
metric name is malformed, or redeclared with a different kind, bounds or observer | a name that is not lowercase snake_case — dotted OTel names survive OTLP but not a Prometheus scrape, and the autoscaler reads the scrape — or one name declared as both a counter and a gauge, or a second histogram('x', { bounds }) / gauge('x', { observe }) whose stated option differs from the first. An omitted option is still a handle-fetch, so gauge(name) keeps working |
rename the instrument: counter('http_requests_total'), one metric name to one kind; or make the second declaration state the same bounds/observe as the first |
X_METRIC_VALUE_INVALID |
metric value is not recordable | a NaN/Infinity observation, or a counter decremented — a counter is a cumulative total and only goes up |
pass a finite value, and use gauge(name) for a number that can fall |
X_METRIC_CARDINALITY |
a metric exceeded its series ceiling and is folding into one overflow series | an unbounded label at the call site — a user id, a path, an email — so every distinct value mints a series and the scrape grows without limit. Past maxSeries every further label set folds into one otel_metric_overflow="true" series, reported once per instrument rather than once per observation. Also raised at declaration for a maxSeries that is not a positive integer |
drop the unbounded label from the call site, or raise it deliberately: counter('orders_total', { maxSeries: 4000 })
|
X_OTLP_ENDPOINT_INVALID |
the OTLP collector endpoint is missing or malformed |
OTEL_EXPORTER_OTLP_ENDPOINT unset with none passed, a value that is not a URL, a scheme that is not http/https, or the collector's gRPC receiver on :4317 — OTLP/HTTP JSON is served on :4318, and an exporter pointed at the wrong port drops every batch with a transport error and no metrics |
set OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318, or skip the exporter when tryOtlpEndpoint('<signal>') is undefined |
X_OTLP_HEADERS_INVALID |
OTEL_EXPORTER_OTLP_HEADERS is malformed |
in OTEL_EXPORTER_OTLP_HEADERS or the per-signal OTEL_EXPORTER_OTLP_TRACES_HEADERS / ..._METRICS_HEADERS that replaces it for its signal — the cause names the variable read — a percent-escape in one header value that decodeURIComponent cannot decode — a stray % meant literally (api-key=100%), or a truncated escape (%zz). Refused rather than kept raw: an operator-set variable with a bad escape is a misconfiguration, and sending the undecoded bytes would authenticate against nothing while looking like a collector outage. The header key is named in the cause and the fix; the value never is, because a header value is a credential |
set <the variable the cause names>=<key>=<encoded>, where <encoded> is what bun -e 'console.log(encodeURIComponent(process.argv[1]))' <value> prints — or drop the stray % if it was meant literally |
X_OTLP_PROTOCOL_UNSUPPORTED |
the OTLP protocol requested is not OTLP/HTTP JSON |
OTEL_EXPORTER_OTLP_PROTOCOL asking for grpc or http/protobuf. This exporter speaks one wire format and no dependency ships a second — refused at construction rather than negotiated down |
unset OTEL_EXPORTER_OTLP_PROTOCOL (or set it to http/json) and point OTEL_EXPORTER_OTLP_ENDPOINT at the collector's HTTP receiver, e.g. http://otel-collector:4318
|
X_TELEMETRY_SAMPLER_ARG_INVALID |
the trace sampling ratio is not a number between 0 and 1 |
OTEL_TRACES_SAMPLER_ARG holding a value outside [0, 1], or one that is not a number. Read at the first span, not at boot — so it warns through the logger and samples every trace rather than killing a process mid-request over a sampling typo. Losing the traces silently is the worse failure; the bill is the loud one |
set OTEL_TRACES_SAMPLER_ARG=0.05 — the cause names the spelling it read |
One pipeline in @ultimat3/core serves storage, seo and pwa. It decodes PNG, JPEG, WebP and GIF and encodes PNG, JPEG and WebP (DECODABLE_FORMATS / ENCODABLE_FORMATS, As of 2026-10); AVIF and SVG are identified and measured — enough to inline width/height and keep CLS at 0 — but never synthesised. A variant in one of those formats comes from a CDN or an ImageTransformDriver, never from a silent passthrough.
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_IMAGE_UNSUPPORTED |
the built-in image pipeline cannot read or write this format | a request for an AVIF or HEIC variant (both need an OS codec the pinned backend: 'bun' never uses), an SVG or TIFF source, or a colour that is not hex or transparent
|
await transformImageBytes(bytes, { format: 'png' }) (or 'jpeg', or 'webp'), or pass an ImageTransformDriver that produces the format — meta.format names the one refused |
X_IMAGE_DECODE_FAILED |
image bytes are malformed, truncated or internally inconsistent | a partial upload, a corrupted file, a header declaring no size, or a header that disagrees with the data that follows — a PNG stream that inflates past what its header's size needs is stopped at that bound |
file <path> to confirm the type, then re-export the image and retry |
X_IMAGE_TOO_LARGE |
image exceeds the pipeline pixel ceiling | a header declaring more than 64 megapixels — usually a decompression bomb, occasionally a real scan. A header declaring NO size (zero, negative, fractional) is X_IMAGE_DECODE_FAILED, not this |
downscale the source below the 64-megapixel ceiling before it reaches the pipeline, or route it through an ImageTransformDriver (a CDN or an external encoder). MAX_IMAGE_PIXELS is fixed, not a setting |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_CONFIG_INVALID |
a configuration this process cannot boot on — env or app.config.ts
|
a field failed its schema, a required field is missing, or two keys that each parse contradict each other (selectMailDriver, selectPurgeDriver), or a declaration whose guarantee this build cannot enforce (defineAuth({ mfa: { required: true } }) — nothing reads it, so believing it would be a false security claim) |
x doctor --json and fix the named key; see Configuration
|
X_ENV_MISSING |
required environment variables are missing or invalid | a key absent at boot; validation runs before the server listens |
x env check --json, then set the keys it names. There is no --fix: x env declares check and example and no flags of its own (packages/cli/src/cmd-env.ts), so the flagged form dies at the parser with X_CLI_BAD_FLAG
|
X_ENVIRONMENT_INVALID |
ULTIMATE_ENV is not a known environment |
a spelling the framework does not use (prod, dev, preview). NODE_ENV is only a fallback and is never policed — CI images legitimately set it to anything |
export ULTIMATE_ENV=production — one of development, test, staging, production
|
X_ENV_EXAMPLE_DRIFT |
.env.example does not declare every variable the schema requires |
a key added to defineEnv() and not to the committed example, so the next clone boots into X_ENV_MISSING for a variable nobody documented |
x env example — the example is a projection of the schema, never a second list |
X_BUN_VERSION |
Bun is older than the framework floor | the runtime is below the floor — x doctor checks the CONSUMER floor (packages/cli/src/app-root.ts), scripts/setup.ts checks the CONTRIBUTOR floor, which tracks CI's pinned series and is higher |
bun upgrade |
X_NOT_IN_APP |
command must run inside an Ultimate app | no app.config.ts at or above the cwd |
x new myapp && cd myapp |
X_PORT_IN_USE |
the dev port is taken | an unrelated process holds it — usually another project's dev server; the pid and command are named when the OS will say |
x dev --port 3001, or kill the named pid if it is yours. The scrape port on a deployed role: set METRICS_PORT=<free port> in this role's environment, one per role on a host; the refusal stays, a scrape target must not move on its own |
X_DEV_ALREADY_RUNNING |
another x dev already owns this checkout | a live x dev holds .x/ — embedded Postgres is a single-writer data directory, so a second one cannot open it |
use the one already running, or stop it: x stop --json
|
X_DEV_LOCK_UNREADABLE |
the dev lock file cannot be read or removed |
.x/dev.lock parses as no lock and unlink refused it — a directory left at that path, or a file this user may not delete. Its own code because the alternative was X_DEV_ALREADY_RUNNING naming the READER's own pid as the holder, whose fix: was kill <self>
|
rm .x/dev.lock, then re-run x dev
|
X_PORT_INVALID |
PORT is not a TCP port number |
a platform-injected PORT that is empty, non-numeric or out of range — never defaulted past, because a web role that quietly bound 3000 fails the platform's health probe with nothing in the log that names the cause |
docker run -e PORT=3000 <image> |
X_ROLE_UNKNOWN |
ROLE names something that is not a role |
a typo in a compose file, a Helm value or a docker run — one image runs every role, so the typo would otherwise start a process that serves nothing and reports healthy |
docker run -e ROLE=web <image> — one of web, sync, worker, scheduler, replicator, migrate
|
One committed file, secrets.enc.json, sealed with AES-256-GCM under a 32-byte master key read from ULTIMATE_SECRETS_KEY first and the gitignored .secrets.key second. Values are decrypted into the process environment by installSecrets() and read back through the app's own envSchema — a secret is an env var, so it has one declaration, one mask and one reader. See CLI reference for x secrets.
The envelope carries a key id: a domain-separated, truncated SHA-256 of the master key, safe to commit. It is what makes "you used the wrong key" and "this file was edited after it was sealed" two different codes instead of one unexplained decryption failure.
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_SECRETS_KEY_MISSING |
no master key for the encrypted secrets file |
ULTIMATE_SECRETS_KEY is unset and .secrets.key does not exist, while a sealed file does. Fatal at boot on purpose — an app that started without its secrets authenticates against nothing and still reports healthy |
export ULTIMATE_SECRETS_KEY="$(cat .secrets.key)", or x secrets init in a repo that has no key yet |
X_SECRETS_KEY_INVALID |
the master key is not 32 bytes of hex | a truncated paste, or a platform secret that carried quotes. An AES-256 key is exactly 64 lowercase hex characters. The same code for a malformed entry of ULTIMATE_SECRETS_RETIRED_KEYS, whose cause names the entry |
by where the bad key was READ: from ULTIMATE_SECRETS_KEY, export ULTIMATE_SECRETS_KEY="$(cat .secrets.key)"; from the key file, wc -c <the key file> — 65 is a whole key and its newline, any other count is the file to restore from wherever the team keeps the key (re-exporting reads the same bad file, and a lost key cannot be regenerated); for a retired-ring entry, x secrets edit and correct or remove the entry the cause names in ULTIMATE_SECRETS_RETIRED_KEYS — the variable is a line of secrets.enc.json, written there by x secrets rotate; a platform that also sets it wins, so correct it there too |
X_SECRETS_KEY_MISMATCH |
the secrets file was sealed with a different master key | a key from another environment, or a x secrets rotate interrupted between its two writes. Both key ids are named in the cause |
git checkout -- secrets.enc.json, or point ULTIMATE_SECRETS_KEY at the key whose id the file names |
X_SECRETS_FILE_MISSING |
the encrypted secrets file does not exist |
x secrets show/edit/set/rotate in a repo that never ran init. Never an empty listing — "no secrets" and "no file" are different facts |
x secrets init |
X_SECRETS_FILE_INVALID |
the encrypted secrets file is not a readable envelope | a merge conflict marker, a truncated write, an unknown v or alg, or a header field that is not base64. Nothing reached authentication, so this is not tampering |
git checkout -- secrets.enc.json — the file is written only by x secrets, never by hand |
X_SECRETS_TAMPERED |
the secrets file failed its authentication tag | under a key whose id already matched, the ciphertext or the header bound into it as AAD changed after sealing. GCM refuses rather than returning garbage |
git checkout -- secrets.enc.json, then x secrets show --json
|
X_SECRETS_PLAINTEXT_INVALID |
the decrypted secrets are not a flat map of env values | an x secrets edit buffer saved as something other than one flat JSON object, a key that is not an environment variable name, or an empty value. Refused before sealing, so a file that opens always installs |
x secrets edit — the buffer is {"SESSION_SECRET": "s3cr3t"}
|
X_SECRETS_EDITOR_MISSING |
no $EDITOR to open the decrypted secrets in |
neither VISUAL nor EDITOR is set. No editor is guessed — opening a decrypted file in a surprise program is the last place a default belongs |
EDITOR=nano x secrets edit, or write one value without an editor: printf %s "$TOKEN" | x secrets set STRIPE_KEY
|
X_SECRETS_EDIT_FAILED |
the editor exited non-zero, so nothing was resealed | a crash or a deliberate abort. The buffer is discarded and the committed file is untouched | x secrets edit |
X_SEAL_KEY_MISSING |
no master key to seal or open a value with |
seal() / open() ran with ULTIMATE_SECRETS_KEY unset and no .secrets.key under the app root. Never a pass-through — a value that should have been sealed is refused, not stored as plaintext |
x secrets init, or where the key already exists export ULTIMATE_SECRETS_KEY="$(cat .secrets.key)"
|
X_SEAL_KEY_UNKNOWN |
a sealed value names a master key this process does not declare | a retired key was dropped (x secrets rotate --drop <keyId>) before the re-seal backfill() finished, or the value was copied from another environment. The cause names the value's key id and lists the declared ids, current first |
x secrets edit — put the retired key back in ULTIMATE_SECRETS_RETIRED_KEYS until the re-seal backfill() has finished |
X_SEAL_INVALID |
a sealed value did not authenticate, or is not a sealed value | the string is not x1.<keyId>.<iv>.<ciphertext> (never sealed, or truncated), or the AES-256-GCM tag rejected it under a key whose id matched: it was sealed for a different purpose, or its bytes changed. AEAD cannot tell those two apart, so the cause names both. Never garbage, never the raw string back |
x secrets show --json — confirms the key id in force; a value that failed its tag is unrecoverable and must be re-entered. Never sealed (a column sealed after rows were written): add a new .sealed() column, copy with a backfill(), drop the old one — Entities and migrations
|
X_WORKSPACE_DEP_UNDECLARED |
a workspace imports another workspace its own package.json does not declare |
an import added long after the scaffold, or a generator that emitted the import and not the edge. The graph then exists only inside tsc — affected-package detection, bun --filter ordering and blast radius all read manifests, and a missing edge makes each of them silently answer too small a set. Shipped source only: a test file's import is not judged, because packages/* here declares no devDependencies by design and the root's hoist is what resolves them |
add the dependency the finding names — it carries the manifest path and the exact line, e.g. "@demo/web": "0.0.0"
|
X_PACKAGE_DUPLICATED |
two copies of one registry-holding framework package (@ultimat3/i18n, @ultimat3/policy, @ultimat3/entity) are installed in one app |
a workspace pinning an older range than the root (packages/i18n on ^19.0.0, the root on ^19.1.0), or two resolutions of one version. Each copy is its own module instance with its own registry, so the app registers into one and the CLI reads the other: x i18n check reported X_CATALOG_UNREGISTERED over a defineCatalogs() call that was exactly where its fix said to put it, and the policy step is green over an undeclared grant because isKnownPermission checks nothing while its own copy holds no declaration. Judged by realpath — a workspace symlink to one checkout is one copy |
the finding names both copies and the package.json to pin: set every @ultimat3/* to one exact version there, then bun install; two copies of ONE version are collapsed by bun install --force. x i18n check --json and x verify --only policy --json re-check |
X_SHOT_BROWSER_MISSING |
x shot has no browser to launch |
none was named — --browser, PUPPETEER_EXECUTABLE_PATH, CHROME_PATH — and none of the four probed paths (/usr/bin/google-chrome and its siblings) is on disk. Raised before the dev server boots. As of 22.0.0 x shot drives Chrome over raw CDP, with no browser library in the app: it no longer means puppeteer-core is missing |
export CHROME_PATH=/usr/bin/google-chrome — or attach to a browser somebody else runs: x shot / --cdp-url wss://cdp.example.com/session/abc
|
X_SHOT_HOST_REFUSED |
x shot was asked to open a host outside the run's allow list | a goto to a host no allowHosts rule matches, refused before Page.navigate is sent. A request the PAGE makes to such a host is not this code — it is paused in the browser (Fetch.requestPaused), failed, and recorded in verdict.json as refused: "host"
|
x shot /route --allow-hosts cdn.example.com — the app's own host is always allowed |
X_SHOT_ELEMENT_MISSING |
no element matched the selector before the deadline | a ui.interact step or a waitFor whose selector matched nothing for the whole budget — the markup changed, or the selector was never right. Its twin below is the other question: it matched and could not be acted on |
run ui.inspect on the route first and copy a selector it reports with count >= 1
|
X_SHOT_ELEMENT_UNREADY |
the element matched and never became ready to act on | the first match stayed hidden, zero-sized, disabled, covered at its centre by another element, or still moving between two polls. The cause names which | run ui.inspect with the selector and "a11y": true to see what covers or disables it, then act on that element first |
X_SHOT_KEY_INVALID |
a key chord the browser cannot press | an empty chord, a trailing +, a modifier other than Meta/Control/Alt/Shift (Ctrl is refused), one held twice, or a key that is neither one character nor a named key. Parsed before any key goes down, so no modifier is left held |
spell it as modifiers then one key: Meta+K, Control+Enter, Shift+Tab, Escape
|
X_GH_UNAVAILABLE |
the GitHub CLI is not runnable from here |
gh is not installed or not on PATH. x pr and x ci shell out to it rather than embedding an HTTP client — no new dependency, and the user's existing auth |
gh auth login — install first from https://cli.github.com
|
X_GH_NOT_AUTHENTICATED |
gh holds no credentials for this host |
a fresh machine, an expired token, or a GitHub Enterprise host gh was never logged into. Detected from gh's own refusal text, so it is never confused with a network failure |
gh auth login |
X_GH_COMMAND_FAILED |
a gh invocation exited non-zero |
the invocation and its exit code are in the cause. gh exits non-zero whenever a GraphQL response carries errors, so a partial answer never reaches the parser |
the finding carries the exact gh invocation that failed — run it yourself to see why |
X_GH_RESPONSE_INVALID |
gh's output is not the shape the command reads |
GitHub changed a field, or a query was edited without its schema. The response is parsed, never cast: a null where an id was expected would otherwise become a resolve call against undefined
|
the finding names the field that did not parse; x pr review --json
|
X_PR_NOT_FOUND |
no pull request for this checkout |
x pr with no --pr, on a branch GitHub has no PR for |
x pr review --pr <n> --json, or open one first: gh pr create
|
X_CI_RUN_NOT_FOUND |
no workflow run for this branch |
x ci on a branch that has never been pushed, whose runs have aged out, or whose --workflow/--event filter matched nothing (the cause names the filter given) |
x ci --branch main --json |
X_SEAL_RESEAL_PENDING |
sealed columns still hold values under a retired master key |
x doctor: the app declares .sealed() columns and ULTIMATE_SECRETS_RETIRED_KEYS still declares a retired key — a rotation whose re-seal has not finished. Until it has, a lookup column matches through in (…) over every key and its .unique() is only as strong as one key: one value can be stored once per key |
re-seal with a backfill() that rewrites each row (update(id, { <column>: row.<column> }) seals under the current key), then x secrets rotate --drop <keyId>
|
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_VALIDATION_FAILED |
value did not match its schema | a parse boundary rejected input | read cause for the failing path; correct the value or widen the schema deliberately |
X_SCHEMA_UNSUPPORTED |
the active schema provider cannot do this | a Standard Schema implementation without JSON Schema export | drop the configureSchemaProvider() call and use t, the shipped dependency-free builtin provider |
X_SCHEMA_DISCRIMINANT_INVALID |
a discriminated union member can never be dispatched to | a member of t.discriminatedUnion('<key>', […]) that declares no literal at the discriminant, or one whose tag an earlier member already claims. Thrown where the union is built, not where a value is parsed: a member no tag routes to is wrong for every input, so the first import of the authoring file is the earliest honest place to say so |
give the member named in cause a literal discriminant — t.object({ <key>: t.literal('…'), … }) — its own value at that key, or merge the two members into one; t.union(...) if the members share no key |
X_SCHEMA_DEFAULT_UNSHAREABLE |
a schema default cannot be copied per parse |
.default(value) where value holds a function, a symbol or anything else structuredClone refuses. Every parse that omits the field must receive its OWN copy — one shared object meant a handler's mutation became the next request's starting value — so a default that cannot be copied is refused where it is WRITTEN, at the first import of the authoring file, rather than shared and bled |
pass a JSON-shaped default (plain object, array, Date, Map, Set), or drop .default() and answer the absent value in the handler |
X_SCHEMA_DEFAULT_INVALID |
a schema default fails its own schema |
.default(value) where value is one the schema itself refuses — t.number.min(5).default(1). An omitted field then parsed to a value the same schema rejects when it is sent, and the published JSON Schema stated a default that violates its own bounds, so it is refused where it is WRITTEN, at the first import of the authoring file |
edit the .default(…) named in the stack so its value satisfies the rule quoted in cause, or relax that rule on the schema — then bun test <the authoring file>.test.ts
|
X_SCHEMA_BOUNDS_INVALID |
an array bound no array can satisfy | a t.array(items, { min, max }) whose min or max is not a whole number from 0 up, or whose min is above its max. Thrown where the array is declared, not where a value is parsed: a bound no count satisfies is wrong for every input |
t.array(items, { min: 1, max: 20 }) — whole numbers, 0 <= min <= max; omit a bound to leave that side open |
X_SCHEMA_SENSITIVE_OUTPUT |
a sensitive field is reachable from an output | a schema field marked .sensitive() (writeOnly: sent in, never sent back) is reachable from an action output or a query rows schema, and an output leaves the server | remove .sensitive() from the path named in cause, or drop the field from the output schema, then: x verify |
X_SCHEMA_ISSUE_CAP_INVALID |
a validation issue cap no list can meet | validate()/validateAsync() was passed a maxIssues that is not a whole number from 1 up | validate(schema, value, { maxIssues: 100 }) — or omit maxIssues for the default MAX_ISSUES |
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_ROUTE_NOT_FOUND |
no route matches this request | path typo, or the route was never generated. Under /api/ no route file applies: every path there is an action or a query defineApi() mounts |
x routes --json, then x g route <path> — under /api/, x routes --json alone: call an endpoint it lists |
X_METHOD_NOT_ALLOWED |
route exists but not for this method | GET against an action endpoint | call it with the method in cause, or add that method |
X_PATH_INVALID |
a path segment is not valid percent-encoding | a hand-built URL, or a value concatenated into a path instead of run through encodeURIComponent — %ZZ, a lone %, a truncated %A. Answers 400: the client wrote the path, so the client is who can fix it, and the request never reaches a handler |
send the segment percent-encoded — encodeURIComponent(value); a literal % is %25
|
X_ROUTE_CONFLICT |
two routes claim the same path | a copied route file |
x routes --json, then remove or rename one |
X_BODY_INVALID |
request body failed its schema | a plain runtime.ts route declaring meta.input was sent the wrong shape — an action or query answers X_INPUT_INVALID instead |
x routes --surface api --json to find the route, then send a body its meta.input schema accepts |
X_UNAUTHENTICATED |
no authenticated actor for this request | the two things that produce an anonymous actor where one is needed: the request carried no __Host-x_session cookie, or it carried one nothing resolved. authenticate() answers the anonymous actor for a missing cookie rather than throwing, which is why the failure surfaces one layer later, at the surface that needed the actor. Also the code for an oauth identity a local account already holds — @ultimat3/auth owns it, @ultimat3/http only borrows it |
send the __Host-x_session cookie with this request, and resolve it once at the boundary: authenticate(auth, readSessionCookie(request, auth.sessions.policy))
|
X_FORBIDDEN |
policy denied this actor | the actor exists but the policy said no. A remote caller — a production problem document, an app's MCP tool result — is given the error's callerFix instead: ask the account owner or an administrator for the permission (As of 22.10) |
x policy explain <path> --json |
X_RATE_LIMITED |
rate limit exhausted for this key | bucket empty. One request spends a LIST of keys — the caller's, then the tenant's when rateLimit.tenantBucket is declared — and stops at the first refusal, so meta.key names which one |
retry after Retry-After, or raise the bucket: configureHttp({ rateLimit: { buckets } }) at module scope in a file under apps/*/
|
X_RATE_LIMIT_NOT_SHARED |
the app declared a fleet-wide rate limit and the store keeps its counters per process |
rateLimit.scope: 'shared' against memoryRateLimitStore(), whose scope is 'process'. assertRateLimitScope compares the two inside httpPipeline, before the socket opens — the outcome it PREVENTS is N replicas each enforcing the full bucket, so a declared limit: 5 costs 5 × N, silently. Also raised when enabled: false sits under scope: 'shared': a fleet-wide limit switched off is a claim with nothing behind it |
install a shared store — httpServer({ routes, rateLimitStore: postgresRateLimitStore({ executor }) }) — or drop to scope: 'process' and terminate the limit at a shared proxy instead |
X_RATE_LIMIT_BUCKET_CONFLICT |
one bucket has two declarations that disagree | an action's rateLimit and a rateLimit.buckets.<name> entry passed to configureHttp() name different numbers, or two routes claim one bucket name. Refused at httpServer/httpPipeline, before the socket opens — picking a winner leaves the loser a number someone reads and nothing applies. An identical restatement passes |
one edit, not a choice: delete rateLimit.buckets.<name> from the app's configureHttp() call. The route is authoritative because it sits beside the handler and is what the OpenAPI operation publishes, so a config entry duplicating it is the copy that goes stale — the cause says so, and says which numbers came from where |
X_RATE_LIMIT_BUCKET_UNBOUND |
the installed limiter does not hold a bucket a route declares | a limiter passed through PipelineDeps.limiter was built by the app from a config that never saw the routes, so every route-declared limit silently reverted to default — measured before the fix: 21 of 21 requests allowed against a route declaring 5, with ratelimit-limit: 120 on the wire. Refused rather than rebound, because a RateLimiter is opaque and "binding" it would mean discarding the caller's limiter and its store. An absent bucket table is refused exactly as a wrong one is: what cannot be shown to hold is not assumed to hold |
pass the store and let the pipeline build the limiter — httpServer({ routes, rateLimitStore }) — so the bucket table is the one the routes registered |
X_RATE_LIMIT_SCOPE_UNSET |
the deployment has not said where the rate limiter keeps its counters | an enabled rateLimit with no scope, at defineHttpConfig. Reachable only from an embedder that builds its own config: scope is boot-owned, so configureHttp() type-refuses it and a process booted by x dev or apps/web/server.ts DERIVES it from the store it installed. scope used to default to 'process', so login: { limit: 5 } against the 3 web replicas this repo's own chart ships enforced 15 per window, with a green x verify and nothing anywhere to read. assertRateLimitScope only ever fired for an app that said 'shared', so the silent case was exactly the one nobody declared. One process is still a legal answer; it is no longer an assumed one |
defineHttpConfig({ rateLimit: { scope: 'process' } }) if this app runs as ONE replica, or scope: 'shared' plus httpServer({ routes, rateLimitStore: postgresRateLimitStore({ executor }) }) for a fleet-wide limit |
X_RATE_LIMIT_TENANT_BUCKET_UNKNOWN |
the tenant allowance names a bucket nothing declares |
rateLimit.tenantBucket: 'tenant' with no rateLimit.buckets.tenant beside it. Refused at defineHttpConfig, before the socket opens, and not on the first request: bucketFor resolves an unknown name to default, so a whole tenant's cap written as 5,000 would silently be the 120-burst default — looser than the declaration and visible nowhere. The cause lists every bucket name that IS declared |
declare the bucket beside the name — configureHttp({ rateLimit: { tenantBucket: '<name>', buckets: { '<name>': { capacity: 5000, refillPerSecond: 100 } } } }) — or drop tenantBucket to leave this app with no per-tenant allowance |
X_RATE_LIMIT_INVALID |
a declared rate limit computes to numbers the limiter cannot run on | a { limit, windowMs } pair toBucket cannot convert: limit below 1 refuses the first caller, which is a policy's job and never a rate limit's, and windowMs: 0 or { limit: MAX_VALUE, windowMs: 1 } compute an infinite refill — a bucket that never empties. Raised in @ultimat3/http, which owns Bucket and the maths, so an action's and a query's rateLimit: are refused by one check and not two |
edit the rateLimit: on the declaration cause names to a whole allowance over a real window — { limit: 5, windowMs: 600_000 } for five per ten minutes — or delete it to keep the default bucket |
X_RATE_LIMIT_STORE_UNAVAILABLE |
the shared rate-limit store did not answer, so nothing decided | the PgExecutor handed to postgresRateLimitStore returned no row for a statement whose returning clause always produces one — a swallowed driver error, a pooler that dropped the statement, or an executor that maps returning away. Raised rather than defaulted: a limiter that cannot read its bucket has no safe assumption, and guessing allowed hands out the fleet-wide budget to every replica at once |
psql "$DATABASE_URL" -c "select * from x_rate_limit limit 1" — then confirm the PgExecutor passed to postgresRateLimitStore returns the rows of returning
|
X_TRUST_PROXY_UNSET |
proxy headers are trusted without saying how many proxies are in front |
trustProxy: true with no trustedProxyHops. The leftmost x-forwarded-for entry is whatever the client typed, so without a hop count there is no way to tell the proxy's entry from the caller's — and every per-ip limit and lockout is then keyed on an address the caller chooses. Asked at defineHttpConfig in the same shape as X_RATE_LIMIT_NOT_SHARED, because only the app knows its own topology. Both keys are boot-owned and configureHttp() type-refuses them: a framework-booted process reads the hop count from the ENVIRONMENT, because one image runs behind an ingress in one cluster and behind nothing on a laptop |
set TRUSTED_PROXY_HOPS in the deployment environment to the number of proxies that append to x-forwarded-for — 1 for a single ingress or ALB, 2 for a CDN in front of one — and leave it unset for a process reached directly (trustedHopsFromEnv, packages/cli/src/role-start.ts). An embedder calling defineHttpConfig itself passes { trustProxy: true, trustedProxyHops: 1 }
|
X_OVERLOADED |
in-flight requests are at the configured ceiling | more concurrent requests than maxInflight (1,000). Shed before any work — no route match, no auth, no body, no query — because the alternative is not "serve everyone" but "serve nobody": every request queues behind the same pool, p99 walks off the chart and client retries multiply the load. @ultimat3/realtime's AcceptBudget is the same decision for sockets. Classified retry-after, and the response carries the header; also thrown by @ultimat3/auth's KDF gate, which refuses a password hash when 8 are running and 64 are queued — each argon2id arena is ~19 MiB, so the caps bound memory the per-IP limiters cannot |
retry after the Retry-After header; to serve more at once call configureHttp({ maxInflight: 2000 }) at module scope in a file under apps/*/, and add replicas to match. For the KDF half, installKdfGate({ maxConcurrent, maxQueued }) from @ultimat3/auth/adapter, in apps/web/runtime.ts
|
X_CSRF_BLOCKED |
an unsafe request that did not prove same-origin | a state-changing request a browser sent — carrying its ambient session cookie, or anonymously (a forged sign-in) — whose Origin is not one this app allows. Never a 401: who is calling was never the question. An anonymous request carrying neither Origin nor sec-fetch-site is not judged |
from a loopback client, the curl -H 'sec-fetch-site: same-origin' line it prints, with the request's own origin and path. Otherwise call it with an Authorization: Bearer token instead of the session cookie, add the calling origin to configureHttp({ cors: { origins } }), or configureHttp({ csrf: { mode: 'off' } }) if this app has no cookie session at all |
X_WEBHOOK_SIGNATURE_INVALID |
the inbound webhook is not signed by the holder of this secret | four conditions, one code: no readable x-ultimate-webhook-signature, an id or topic header outside 1–200 characters or carrying a : or a control character, a mac that does not match the bytes that arrived, or a secret the two sides disagree about. The header check is refused before the mac is taken — the signed string is v1:<t>:<id>:<topic>:<body>, so id evt:01HZ with topic orders.paid and id evt with topic 01HZ:orders.paid build the SAME bytes, one mac authenticating two differently-labelled deliveries. 401 and never 400: the request is well formed and carried a CREDENTIAL, so what failed is authentication. Never 403 either, which means an authenticated caller was refused, and there is none here. Does not trigger signInRedirect, which keys on X_UNAUTHENTICATED alone — a sender is not a browser and has no session to go get. cause names only what the framework chose: never the signature, never the secret, never the body |
sign the delivery with the secret this endpoint was registered under, or re-read the secret from your sender dashboard and pass it as verifyWebhookSignature(request, { secret })
|
X_WEBHOOK_SIGNATURE_STALE |
the inbound webhook is signed correctly and is too old to accept | the mac verified and the timestamp sits further than toleranceMs (default 300,000 ms) from this clock — a sender's clock, or a delivery replayed off a capture. The mac is checked before the window, deliberately, so this code always means "authentic and old" and never "unreadable and old". Both directions of skew count: a sender running ahead is the same replay window pointed the other way, and accepting the future half doubles it. Same 401 as above — the credential is a timestamped one and this is its expiry half |
sync the sending host clock with NTP, or widen the window with verifyWebhookSignature(request, { secret, toleranceMs: 600_000 }) if the sender queues deliveries for longer than that |
X_BUILD_SKEW |
client build id does not match the server build id | a tab open across an incompatible deploy | reload; the SW fetches the new build manifest. See PWA and offline |
X_SERVER_NOT_STARTED |
server handle used before start()
|
reading url() too early in a test |
await httpServer({ … }).start() first |
X_PIPELINE_NO_RESPONSE |
a pipeline stage produced no response | a middleware returned undefined
|
return a Response from the stage or from the handler |
X_PIPELINE_FINALIZE_FAILED |
a finalize stage threw instead of finishing the response | the response cannot take the headers the pipeline puts on it — a Response.redirect(), or a frozen or proxied Response a handler returned. handle() answers this 500 rather than rejecting, and the cause names the stage |
return a Response built here — jsonResponse(), textResponse(), html() or redirect() from @ultimat3/http
|
X_NO_REQUEST |
the inbound request is not in scope here |
useRequestCookie() / setRedirect() called from a job, a task or a module body |
read it inside a route handler, an action or a page; a job gets the value from its payload |
X_ERROR_STATUS_INVALID |
an error code cannot be mapped to that status |
registerErrorStatus() given a framework-owned code, a status outside 100–599, or a second, different status for one code |
x errors list --json, then map a code this app owns to a status the framework does not already hold |
X_PROBLEM_META_INVALID |
a problem document cannot carry that meta declaration |
registerProblemMeta() given a framework-owned code (its meta is operator-only — the body excerpt bodyInvalid keeps there, the limiter's key), a key that is not one (issues already rides at the document's top level; __proto__), an empty list, or a second, different list for one code |
registerProblemMeta({ X_YOUR_CODE: ['sessionId'] }) with the keys your own error puts in meta, beside its registerErrorStatus() call — both are needed, because a code with no status is an unclassified 5xx whose meta is blanked with its cause
|
X_CORS_CONFIG_INVALID |
the cors config can never produce a working response |
cors.origins contains '*' while cors.credentials is true — a pair no browser accepts, which used to resolve to no CORS headers at all rather than an error — or contains 'null', the origin every sandboxed iframe sends, which URL.parse('localhost:3000').origin produces |
call configureHttp({ cors: { credentials: false } }) at module scope in a file under apps/*/, or replace origins: ['*'] with the exact origins allowed to call this app; for 'null', list the scheme: configureHttp({ cors: { origins: ['http://localhost:3000'] } })
|
X_CSP_DIRECTIVE_INVALID |
a csp extension would emit something other than the directive it names | a security.csp.extend key that is not a CSP directive token, or a source carrying one of the header's own separators — { 'x; script-src *': [] } is not one badly named directive, it is a second directive nobody declared, widening the one the framework locks down hardest. Refused at defineHttpConfig, because there is no encoding for a CSP directive and escaping it at emission time is not a repair |
in the configureHttp({ security: { csp: { extend } } }) call write one entry per directive, each source its own array element — a directive name is [a-z][a-z0-9-]*, and no source may contain a space, a comma or a semicolon |
X_BEARER_MOUNT_INVALID |
a bearer mount declaration cannot be served as written | defineApi({ http: { mounts } }) named a primitive nothing registered, or a prefix that would shadow a framework namespace | fix the mount named in the cause: a prefix like '/v1' (not /api or /_x), and a scopes map naming only registered actions and queries — x routes --json lists them |
X_RATE_LIMIT_STORE_TIMEOUT |
the shared rate-limit store did not answer inside its own deadline | the database the shared rate-limit store counts in did not answer a take or peek within postgresRateLimitStore deadlineMs (250 ms by default), so this pod decided that request from its own memory |
x doctor --json — names the database binding this pod cannot reach; a database that is slow rather than gone takes postgresRateLimitStore({ executor, deadlineMs }) raised in the boot that builds it |
X_EXERCISE_REQUEST_INVALID |
an exercise entry's request is not one a browser could send | a request: in apps/web/exercise.ts names a cookie outside the token set, puts a Cookie header beside cookies, or a header Headers refuses |
defineExercise({ <name>: { request: { cookies: { <token>: '<value>' }, headers: { '<x-name>': '<value>' } } } }) — in apps/web/exercise.ts |
X_BODY_TOO_LARGE |
request body over the transport ceiling | the request declares or streams more bytes than http.maxRequestBodyBytes, the most this process reads for any route; a signed upload bigger than that cannot be accepted in one request | send it in parts (storage signPart/completeUpload for an upload), or raise the ceiling: configureHttp({ maxRequestBodyBytes: <bytes> }) in the app's http.ts |
A denial is X_FORBIDDEN, above — @ultimat3/policy owns it and every surface adapter throws it, so there is one code for "the policy said no" wherever it was decided.
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_POLICY_MISSING |
an action was declared without a policy | a new action shipped with no policy:
|
add policy: can('<resource>:<verb>'), or allow('public') to say so explicitly |
X_POLICY_CLAUSE_EMPTY |
a policy combinator was built with no clauses |
and() answered allowed and admitsAnonymous(and()) agreed, so policy: and(...caps.map(can)) over a list that filtered to empty admitted an anonymous caller on all four surfaces — and because meta.auth derives from admitsAnonymous, http did not 401 first. The label rendered as and(), so there was no diagnostic. or() fails closed but denies with no clause allowed this actor, a reason naming no clause. Both are refused where they are written, the allowHosts: [] precedent |
allow('public') is the spelling for "no clauses required"; deny('<reason>') is the one that carries a reason |
X_PERMISSION_UNKNOWN |
permission string is not in the permission set | typo, or a permission never declared | add it to definePermissions([…])
|
X_POLICY_SURFACE_UNKNOWN |
enforce() was handed a surface no adapter answers to |
enforce(surface, policy, args) called with a value outside 'http' | 'live' | 'job' | 'mcp' — a config-driven table, a surface name off the wire, or a JS host, none of which the type reached. Refused rather than dispatched because the adapter table is an object literal and so inherits Object.prototype: adapters['valueOf'] answered a FUNCTION, enforce('valueOf', …) called it with the table as its receiver, and the truthy return was the adapter table typed as a SurfaceDenial — an authz dispatch that failed CLOSED with a denial carrying no code and no reason |
pass one of http, live, job, mcp — enforce('http', policy, args) — or add the adapter for the new surface in packages/policy/src/surfaces.ts, the one place a fifth may be declared |
X_ROLE_REDEFINED |
two modules define the same role differently | one role name declared twice with different grants — a merged feature branch, or a role copied into a second module. Whichever module imported last would otherwise decide what the role can do, and the losing definition is still on disk for the next reader to trust. Both declaration sites are named, because which two is the only thing the author does not already know |
x policy list --json — then keep ONE definition of the role cause names: rename the second, or fold its grants into the first |
X_TENANCY_UNSCOPED |
a tenant-scoped query has no org predicate | a repo call outside any request context, so there is no actor to derive the tenant from; a plan handed to the verify-only assertScoped() while an actor is present; or an upsertAll(rows, { onMatch: 'update' }) whose onConflict omits the tenant column — a collision there lands on another tenant's row |
the cause says which of the two it is. No context: runWithContext(ctxOf({ actor }), fn) from the script or seed. Actor present: build the plan through scopedPlan(), or orgScoped(plan, ctx.actor.orgId). For the upsert: put the tenant column in onConflict
|
X_TENANCY_ACTOR_MISMATCH |
a call named a tenant other than the actor's — as a query predicate, or as the tenant column of a row being written | an orgId that arrived as action input, a query string or a path parameter, passed into a repo call; or an insert/upsertAll row, or an update patch, naming another tenant — the cause names both tenants |
drop the orgId argument, the actor's tenant scopes the plan; or act as that tenant with withChildContext({ actor }); a read that must span tenants is crossTenant('<why>', fn)
|
X_TENANCY_ACTOR_ORG_REQUIRED |
the acting actor carries no tenant | anonymous, or a service actor minted without an orgId, reading a tenant-scoped entity |
mint the actor with its tenant at the request boundary — userActor({ id, orgId }) — or crossTenant('<why>', fn)
|
X_TENANCY_CROSS_DENIED |
a cross-tenant read was entered without the capability |
crossTenant() called by an actor lacking tenancy:cross, or outside every request context — the capability is re-checked on every plan built inside the scope, so an actor swapped in by withChildContext() does not inherit the reach |
mint the actor with the scope — serviceActor({ id: 'reconciler', scopes: ['tenancy:cross'] }) — run inside runWithContext(ctxOf({ actor }), fn), and wrap the reads in crossTenant('<why this spans tenants>', fn); the reason is required and non-blank, because it is what the next reader greps for |
X_ACTION_POLICY_MISSING |
an action was registered without a policy | build-time check on the action registry | add policy: can('<name>') to the declaration |
X_QUERY_POLICY_MISSING |
a query was registered without a policy | same, for reads | add policy: can('<name>') to the query |
X_QUERY_NOT_PAGEABLE |
a read returned rows with no id, so a cursor cannot name a position | a projection or aggregate that drops the primary key — the id is the tiebreak that makes the sort order total | return the key from the query's sql:, e.g. db.posts.select({ id: true, … })
|
X_QUERY_COLUMN_UNSELECTED |
a read filters or sorts on a column its rows do not carry |
from(entity, rows).where({ … }), .compare() or .orderBy() names a column the provider's rows lack — a loader that selected slug and updatedAt and a query filtering on status. It answered [] in silence until 22.0.0: absent is not "not equal", it is a read the loader never made. A present column holding null is a value, and an empty provider judges nothing |
select the column in the loader — db.posts.select({ slug: true, status: true, publishedAt: true }) — or drop the filter the loader already applied |
X_CURSOR_VALUE_UNSUPPORTED |
a sort value cannot be carried in a cursor | a read ordered by a column whose values are objects, or by a number that is NaN/±Infinity — a cursor is JSON, and Date and bigint are the only non-JSON types it tags and revives. Raised where the cursor is MINTED, not where one arrives: the mistake is the read's own orderBy, so no retry and no fresh first page repairs it |
order by a scalar column — .orderBy('createdAt') or .orderBy('id') — and project the composite value into the row instead |
X_AGGREGATE_UNSUPPORTED |
that column has no aggregate both drivers can answer alike | three conditions, one code, and the cause names which. A text column has no min/max the two drivers agree on — ordering is the database's collation in one and JS code-unit order in the other. avg over money is refused outright: the mean of an integer number of minor units is not one, so every answer would be a silent rounding. And a money sum past ±2^53 minor units has no JS number to hold it, and MoneyValue.minor is one |
the fix names the exact call. For a wrong column, <entity>.<fn>('<candidate>') — the cause lists the columns that fn accepts, or says the entity declares none. For avg over money, <entity>.sum(col) and <entity>.count(), dividing at the call site where the rounding is a decision somebody made. For the overflow, narrow the rows with .andWhere(…)
|
X_AGGREGATE_MIXED_CURRENCY |
an amount was aggregated across currencies | 100 JPY and 100 EUR have no common unit, so a sum, min or max over them answers a number in no currency at all. Both drivers count the distinct currencies of the rows they are about to aggregate and refuse past one, rather than answering in whichever came first; the cause lists every currency it found. One currency at two scales is the same refusal with its own words and its own fix — the cause says 2 scales of USD, and a currency filter would keep both rows |
<entity>.andWhere('<col>.currency', 'eq', '<CUR>').<fn>('<col>') — one currency per call — or countBy('<col>.currency') first to see the split. For a scale mix: for await (const rows of <entity>.inBatches(1000)) { … }, folding at one scale in the app |
X_APPROXIMATE_COUNT_FILTERED |
an estimate was asked of a filtered chain |
approximateCount() reads reltuples out of pg_class, which knows nothing about a predicate, so posts.where({ orgId }).approximateCount() would answer for the whole TABLE while reading as "roughly how many of mine". Refused rather than answered, because the wrong answer is plausible and unbounded |
<entity>.count() — the exact answer for a filtered chain. approximateCount() answers for the whole table only |
X_QUERY_CACHE_TTL_INVALID |
a query declares a cache ttlMs no tier can hold |
cache: { ttlMs: Infinity }, 0, a negative value or NaN. Judged at query(), in the file that declared it, rather than on the first read: the tiers already refuse the value with X_CACHE_TTL_INVALID, so before this the mistake compiled, passed review, and made every read of that query fail permanently in production. The cause echoes the value rather than the query name — query() runs before defineApi() stamps a name, exactly as X_QUERY_INPUT_UNENCODABLE does |
pass a positive, finite ttlMs, or drop the cache block — "do not cache" is expressed by not declaring one |
X_QUERY_INPUT_UNENCODABLE |
a query input cannot be carried in a query string | a read declaring a nested t.object/t.record/t.money member, an array or union of one, a REQUIRED t.nullable(...) member, or a top-level input that is not an object, or a member named _first or _after — the two page controls the route reads off the search string before the schema sees it. A read is served as GET /_x/query/<name>, so its input is characters: the typed client would encode a structure as JSON text and skip a null, and coerceQuery has no inverse for either. Raised at query(), in the file that declared it |
flatten the key into scalar arguments (status: t.string, limit: t.number), spell an absent value as .optional() rather than t.nullable(...), or declare it as an action() if it really needs a JSON body |
X_QUERY_SUBSCRIBES_INVALID |
a query declares subscribed relations no live read can use |
subscribes: [], or a subscribes: on a read that is not live: true. Judged at query(), in the file that declared it, like X_QUERY_CACHE_TTL_INVALID one field over — an empty list names no relation for the first subscribe to check, and a non-live read never reaches the cross-check at all, so an accepted one would be a declaration nothing verifies and nothing emits |
name the relation the read selects from — subscribes: ['posts'] — or delete the field; on a read that should be subscribable, add live: true beside it |
X_QUERY_SUBSCRIBES_DRIFT |
a live query subscribes to a relation its own sql never reads | the declaration and the read disagree: subscribes: ['post'] while sql: returns from('posts', …). Thrown at the first subscribe, where the resolved QueryShape is the first thing that can say which relation the read really names — the string lives inside a callback nothing can invoke without valid input. A refusal and not a warning: a declaration that names the wrong relation is one its author reads as true |
add the relation the cause names to that query's subscribes:, then x db gen
|
X_QUERY_SUBSCRIBES_UNKNOWN |
a live query subscribes to a table no entity declares |
subscribes: on a live: true query names a relation this app has no entity for — a typo, or a table that was renamed. Raised by x db gen and the drift step, the one tier holding both the manifest and the entity registry: @ultimat3/query has no table catalog and cannot see it. Since #518 x db gen grants no REPLICA IDENTITY FULL for subscribes: (a keyed table is correct on DEFAULT); the list is validated, never granted |
edit subscribes: on the query the cause names to a table the app declares, or drop the name, then x db gen "retry after fixing subscribes" --json
|
| Code | Means | Typical cause | Fix |
|---|---|---|---|
X_SESSION_EXPIRED |
session passed its idle or absolute expiry |
cause names which of the two clocks ran out |
sign in again, or raise session.absoluteTtlMs / session.idleTtlMs in defineAuth({ session }). The wiki used to name POST /auth/sign-in here; the framework mounts no such route, and the password half is the app's until it ships one |
X_MFA_REQUIRED |
password proven, second factor outstanding | the user has TOTP enrolled, and login() / completeOAuthLogin() throw before any session is written. meta.challenge is a sealed, five-minute credential for the second leg — meta, never cause: both public surfaces publish cause and drop meta, so the sign-in handler decides how it travels |
catch X_MFA_REQUIRED in your sign-in handler, hand error.meta.challenge to the caller, and finish with await completeMfa(auth, challenge, code). No route ships. A wrong code is X_UNAUTHENTICATED and stays counted against the account, address and tenant buckets; past rateLimit.maxAttempts it is X_ACCOUNT_LOCKED. Sealing needs the app's master key — X_SEAL_KEY_MISSING without one |
X_MFA_SECRET_INVALID |
a TOTP secret that is not RFC 4648 base32, so it decodes to zero bytes |
enrolTotp is handed an imported secret outside the alphabet, or totpCode is called with one. verifyTotp does not throw it — an unreadable stored secret reads as a non-verdict there, the same rule verifyAgainst follows for a password hash Bun cannot read, so a login checking a broken row gets a credential refusal rather than a 500 |
issue a fresh one and store it: const { secret } = enrolTotp(auth, { account: user.email }). An imported secret must be base32 (A-Z and 2-7; padding, spaces and dashes are ignored) and must decode to at least one byte. Zero bytes is not a decode that failed closed: an HMAC keyed with zero bytes is a valid HMAC, so every unreadable secret in the table used to verify against one shared code stream that needed no secret to compute |
X_OAUTH_STATE_INVALID |
state, nonce or PKCE verifier did not match | a replayed callback URL, a handshake that expired, or a token minted for another browser | restart the flow at GET /auth/sso/<provider> — a callback URL is single-use |
X_OAUTH_EXCHANGE_FAILED |
the provider refused the exchange or returned no usable identity | wrong client secret, an unregistered redirect_uri, a spent code, or a missing scope. Also a discovery document that declares a different issuer than the one asked for, and a key set with no importable key (the cached keys are kept). Not the user pressing Cancel — that is X_OAUTH_DENIED
|
meta.stage is token or userinfo: for token, match <PROVIDER>_CLIENT_SECRET and the registered redirect_uri, then x doctor --json; for userinfo, restart at GET /auth/sso/<provider>
|
X_OAUTH_TOKEN_INVALID |
the id token failed its signature, issuer, audience or expiry check |
As of 2026-08 the signature is verified too, against the issuer's published key set through a JWKS client — until this cycle the token was decoded and its claims read, so a token minted by whoever posted it to the callback passed every check it faced. Also: a token that is not a three-segment JWT, a payload with no iss/sub/exp/aud, a client id in .env that is not the one the authorize URL was built with, or a skewed host clock |
the fix branches on which check failed — for a signature, confirm the token came from the provider's token URL and was not minted by the caller; for the rest, match <PROVIDER>_CLIENT_ID to the id beginOAuth() used, then restart at GET /auth/sso/<provider>
|
X_OAUTH_PROVIDER_DUPLICATE |
two oauth providers were registered under one id | two registerOAuthProvider calls claiming one id. Refused at boot rather than replacing silently, because the winner would be whichever module imported second — and it decides where every login for that id goes, including which issuers an id token may claim
|
give one of them a different id, or delete the duplicate registerOAuthProvider({ id: '<id>' }) call |
X_OAUTH_PROVIDER_UNKNOWN |
the URL named a provider this app has not enabled | the :provider segment is not a known provider, or is one but was left out of defineAuth({ providers }); also providerFor() reaching for an id nothing registered. One refusal for all of them: distinguishing them would describe the app's configuration to an anonymous caller |
registerOAuthProvider({ id: '<id>' }) if it is not built in, then add that id to defineAuth({ providers: [...] }) — the registration comes first because a segment nothing registered cannot be added to providers at all, so "add it" alone was an instruction that could not be followed. The list the fix prints is what is known here, never the subset this app enabled, and meta carries only { provider }
|
X_OAUTH_DENIED |
the user or the provider declined the authorization |
error= on the callback redirect — nearly always the consent screen's Cancel. Split out of X_OAUTH_EXCHANGE_FAILED so the commonest non-success outcome of a login stops paging on-call as a 502 provider outage |
restart at GET /auth/sso/<provider> and approve the consent screen; nothing is wrong with the app |
X_PASSWORD_WEAK |
strength check rejected the password | too short, or a known-common password | choose a longer, uncommon password — or relax defineAuth({ password: { minLength } })
|
X_ACCOUNT_LOCKED |
per-ip, per-account or per-org bucket is inside its lockout | repeated failed attempts — password or second-factor code. cause names the caller's own key (an address or an email) and the seconds left; a locked org is never named, because its id was looked up from the address the caller typed. The wiki used to name x auth unlock <key>, a command that has never existed |
wait out the seconds cause names — or clear this one bucket: await auth.limiter.recordSuccess('<key>'), await auth.orgLimiter.recordSuccess(orgKey(user.orgId)) for a tenant — or raise defineAuth({ rateLimit }). Never reset(): it drops every bucket in the table, including the spray this lockout is holding |
X_AUTH_LIMITER_NOT_SHARED |
the app declared a fleet-wide credential throttle and the limiter keeps its counters per process |
defineAuth({ rateLimit: { scope: 'shared' } }) against the built-in limiter, whose scope is 'process' — account lockout would be maxAttempts × replicas, and a lockout on one replica invisible to the others. Refused at defineAuth, never at a request |
pass a shared AuthLimiter to defineAuth({ limiter }), or drop to scope: 'process'
|
X_AUTH_LIMITER_POLICY_MISMATCH |
a custom credential limiter enforces different numbers than the app declared |
defineAuth({ limiter }) where the limiter's own policy disagrees with the declared rateLimit on maxAttempts, windowMs or lockoutMs. Auth.rateLimit is what an operator reads as "what this deployment enforces", and an injected limiter could leave nothing behind it. The cause names both numbers and says which half is likely wrong; maxKeys is deliberately not compared, since it bounds one process' table and a shared limiter has no opinion on it |
construct the limiter with the declared number — the declaration is what Auth.rateLimit reports, so it is the half that stays. If the limiter's number is the one this deployment means, change the declaration instead; meta carries { field, declared, enforced }
|
X_API_KEY_INVALID |
key unknown, revoked, expired, wrong, or owned by a user who is gone or disabled | one shape for all of them — a precise message is an enumeration oracle, so which it was is not named here and never will be. The wiki used to name x auth keys list --json, a command that has never existed |
issue a replacement: const { plaintext, record } = issueApiKey({ env, scopes }); await auth.adapter.putApiKey(record) — auth.adapter.listApiKeys(ownerId) through describeApiKey() shows which keys are still live |
X_AUTH_WRITE_FAILED |
an adapter write returned no row, so it cannot be confirmed | an insert … returning * wrote nothing — the table is missing its migration, or a trigger or RLS policy swallowed the row. Also a write one of x_users' own unique constraints refused — a second row at one email, external_id or id, on createUser or updateUser; meta.column names which, and memoryAuthAdapter() and postgresAuthAdapter() answer the same code (As of 2026-10; Postgres answered X_DB_UNIQUE_VIOLATION before). Also an OAuth sign-in whose updateUser returned no row for the provider-verified stamp (emailVerifiedNotStored; X_NOT_IMPLEMENTED before As of 2026-10) — the flow fails closed rather than sign in a user every later login reads as unverified. This page printed x db query until 1.2.0; the db subcommands are gen, migrate, reset, studio, branch and backfill
|
x db migrate, then confirm the table with psql "$DATABASE_URL" -c "select 1 from x_users limit 1"
|
X_MFA_SECRET_UNSEALED |
a stored totp secret is not sealed, so it is never read as one | x_users.mfa_secret holds a value written before second-factor secrets were sealed at rest (or written past saveTotpSecret); the framework does not read a plaintext secret, so that account cannot finish a second factor until the value is sealed | x auth seal-mfa --json |
X_AUTH_SIGN_IN_UNCONFIGURED |
a people-facing role started with no sign-in settings outside development and test | x g sign-in wrote a boot guard, and a web or sync role started with the sign-in env keys unset |
export <KEY>="<value>" for every unset key the cause names — in the role's deployed environment (compose environment:, the platform's secrets), then restart the role |
X_AUTH_SERVER_INVALID |
the authorization server was configured with a field it cannot run on | authorizationServer() was handed a bad issuer, consentPath or lifetimes, or a grant was named without the lifetime, path or store method it needs | authorizationServer({ <field>: … }) # the field the cause names |
X_AUTH_CONSENT_INVALID |
a consent was approved for a request the authorization server refuses | approve() or deny() was handed a request readRequest(url) would refuse — an unknown client, an unregistered redirect, a scope the client never asked for | server.readRequest(url) # on the consent page: render its refusal, approve only what it returned |
X_AUTH_GRANT_DISABLED |
a token request named a grant this authorization server does not enable | the client posted a grant_type that is not in authorizationServer({ grants }) | authorizationServer({ grants: ['authorization_code', 'refresh_token', '<grant>'] }) # with its lifetime |
X_VERIFICATION_PURPOSE_INVALID |
a verification purpose was declared twice or without a template | defineVerificationPurpose() was handed a name already declared, an empty name or no template | defineVerificationPurpose({ name: '<unique-name>', template: 'auth.<name>', ttlMs }) |
X_VERIFICATION_PURPOSE_UNKNOWN |
a verification token was issued or consumed for a purpose nothing declared | issueVerification or consumeVerification named a purpose no defineVerificationPurpose() call declared at boot | defineVerificationPurpose({ name: '<purpose>', template: 'auth.<purpose>', ttlMs }) # once, at boot |
X_PASSKEY_INVALID |
a passkey ceremony response failed its signature, origin, rp id, flag or counter check | the assertion or attestation was forged, tampered with, made for another origin or rp id, made without user verification, or came from a cloned authenticator whose signature counter went backwards | x errors explain X_PASSKEY_INVALID --json, then start the ceremony again: await passkeys.loginOptions() |
X_PASSKEY_CHALLENGE_EXPIRED |
a passkey ceremony named a challenge that is unknown, already used or past its ttl | the response was replayed, the options were issued by another ceremony, or the person took longer than the challenge ttl | x errors explain X_PASSKEY_CHALLENGE_EXPIRED --json, then fetch fresh options and run the ceremony once: await passkeys.loginOptions() |
X_PASSKEY_STEP_UP_REQUIRED |
a sensitive action ran without a fresh user-verified passkey proof for its digest | the call carried no assertion, or the assertion approved other bytes than the ones this call would act on | x errors explain X_PASSKEY_STEP_UP_REQUIRED --json, then fetch stepUp.options({ userId, input }), run getPasskey(options) in the browser and resend the call with the response |
X_SAML_RESPONSE_INVALID |
a saml response failed its signature, wrapping, audience, recipient, time, InResponseTo or replay check | the IdP signing certificate rotated, the SP entity id or ACS URL differs from what the IdP has configured, the clocks are skewed, or the response was tampered with or replayed | edit the samlIdentityProvider({ metadata, spEntityId, acsUrl }) call: paste the IdP current metadata XML and the entity id and ACS URL the IdP application is configured with |
X_SAML_METADATA_INVALID |
saml IdP metadata has no entity id, HTTP-Redirect sign-on endpoint or signing certificate | the metadata XML was truncated, is SP metadata rather than IdP metadata, or names only an HTTP-POST sign-on endpoint | download the IdP metadata again from its federation metadata URL and pass the whole XML to samlIdentityProvider({ metadata }) |
X_IDENTITY_PROVIDER_UNKNOWN |
the URL named an identity provider the sso routes were not given | a sign-in link or the IdP ACS URL carries an id that no entry in identityProviderRoutes({ providers }) declares | add the provider to identityProviderRoutes({ providers: [...] }), or correct the id in the link and in the IdP ACS URL |
GET /auth/sso/<provider> is the app's route to mount, not one the framework serves. Every OAuth fix above is reachable only where the host routes matching requests to oauthLogin(auth).start.handle; oauthLogin() supplies the descriptors and nothing else mounts them. The thrown fix: and the descriptor's path both read the one declaration in oauth-paths.ts, so the string cannot go stale — but As of 2026-08 serve.ts composes a fixed set of route tables with no seam for a raw Route, so nothing x new scaffolds mounts the pair and an app that has not mounted it itself answers that path with a 404.
| Code | Means | Typical cause | Fix | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
X_RECORD_KEY_MISSING |
a row reached its record key without a primary-key value | an entity row reached the client-record path, either rowsOf building the record envelope or recordProjection(e).key, and one of its primary-key columns was missing or not a string, number, bigint, boolean or valid Date. A handler built the row by hand, or handed a partial row where the output schema declared the entity's row. Refused rather than keyed as undefined, because two keyless rows would be one record |
x entities show <entity> --json lists the primary key. Return the whole row (every primary-key column) from the handler that built this one |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_DRIFT |
schema differs from migrations | a column exists in code but in no migration — or, on an app that declares an entity and has generated no migration yet, packages/db has a schema but no migration recorded it. An app declaring no entity is not drifting: zero declared against zero recorded is agreement. As of 2026-11-02 also the dump holds a column no migration's snapshot records (drift, no database): an applied migration was edited and regenerated into packages/db/schema/
|
x db gen "<describe the change>" then x db migrate; on a fresh scaffold that is x db gen "initial". A column no entity declares comes out of that as an empty diff: declare it, or restore the migration and x db reset. The dump case: git checkout -- packages/db/migrations packages/db/schema. The one difference this does not fix is unknown-schema — see X_MIGRATION_SNAPSHOT_MISSING below, whose two remedies it shares |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_SCHEMA_UNMIGRATED |
an entity declaration no migration recorded | the half X_DB_DRIFT cannot see: that check compares a schema-source hash and never reads the SQL, so a committed migration smaller than its declaration passes. Measured on dummy/social-media-clone, whose drift was GREEN while nine declared CHECK constraints had never reached any database — a comment body could be whitespace, a like count could go negative, an email needed no @. Compares snapshotOf(describeEntities()) against the newest .snapshot.json, so it needs no database connection. Also names a table a channel with params or a live query's subscribes: needs REPLICA IDENTITY FULL on when no migration recorded it (As of 2026-10-02): that need is a realtime declaration, so no entity hash moves when the channel is added |
bun run x -- db gen "describe the change" — the generated migration is additive, one add constraint per finding |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_SCHEMA_UNDECLARED |
a migration records schema no entity declares | the other direction, and a different repair: the database carries a constraint, column or index the entities no longer ask for. Reported separately from X_DB_SCHEMA_UNMIGRATED because "your database is missing this" and "your last generation lost something" are not one finding |
delete the stale declaration, or bun run x -- db gen "drop what the entities no longer declare" if the removal is intended |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_ENTITY_DUPLICATE |
two entities claim the same name | copy-pasted entity({ name })
|
rename one; x entities list --json
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_INVARIANT_VIOLATED |
a domain invariant rejected this row | a CHECK or a declared invariant failed. This page printed x entity explain until 1.2.0; the registry command is plural and its subcommands are list and show
|
x entities show <entity> --json — every invariant with its kind, message, sql and where
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_NOT_FOUND |
no row for that id | stale id, wrong tenant, or already deleted — or a single: true query's GET /_x/query/<name> whose sql matched no row (404; fix: x queries show <name> --json) |
confirm with psql "$DATABASE_URL" -c "select id from <table> limit 5" — x db has no query subcommand |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_WRITE_UNFILTERED |
deleteWhere(), updateWhere() or a chain's updateAll()/deleteAll() named no filter |
an empty filter, one whose only value came back undefined, or a chain predicate holding undefined — the two look identical at the call site, and either would reach every row |
name the columns that bound it, e.g. db.likes.deleteWhere({ postId, userId }) or db.likes.where({ postId, userId }).deleteAll(). A deliberate whole-table write is a migration: x db gen "<name>"
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_PATCH_EMPTY |
updateWhere(), a chain's updateAll() or updateMany() named no columns to write |
an empty patch, or one whose only value came back undefined — reporting "n rows updated" for a statement that set nothing is the silent no-op the count exists to prevent |
name the columns to write, e.g. db.participants.updateWhere({ conversationId, userId }, { lastReadAt }) or db.participants.where(filter).updateAll({ lastReadAt })
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_PRELOAD_UNKNOWN_RELATION |
no relation of that name on this entity | a typo, or a relation the schema never declared — relations are derived from references() columns, so there is no list to read them off |
the fix is a call on one that exists, e.g. relationNamed('posts', 'author') # or: org, likes. No foreign key at all ⇒ x entities list --json, then add .references(() => <target>.id) to the column that points at one |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_N_PLUS_ONE_QUERY |
a read repeated once per row | one lookup per row of a page — members.findById fifty times where one in statement would answer. Counted per request by the statement ledger x dev installs, and never counted in production. Reported on four surfaces at once: x dev's findings, /_x/timeline's nPlusOne, the browser overlay under the error, and one logger.warn per request per code. Thrown in one place only — a test that destructures @ultimat3/testing's statements fixture, where the loop's fifth statement rejects and fails the test |
the fix is the preload the schema already declares, e.g. db.posts.preload('author'). No relation points at the entity ⇒ the batched form of the same read, db.members.andWhere('id', 'in', ids).all(). A loop that is genuinely one statement per row is declared, not silenced: expectedQueryLoop('<why>', fn)
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_N_PLUS_ONE_WRITE |
a write repeated once per row | one insert, update or delete per row of a set, where one statement writes all of them. Same four surfaces as the read above, same ledger, and the same throw inside the statements fixture |
the bulk form of the same call: db.posts.insertAll(rows), db.posts.updateWhere(filter, patch), db.posts.deleteWhere(filter) — or expectedQueryLoop('<why>', fn) when the loop is deliberate |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_SEARCH_UNDECLARED |
this entity has no searchable column | a matches predicate on an entity whose columns declare no .searchable(), so there is no tsvector to match against. Raised where the STATEMENT would be built and not where the chain was written, because both drivers reach it: a search vector is derived from the entity's own columns, so there is nothing else the predicate could name and no vector to guess at. 500 and not a 4xx — nothing the caller sends changes the answer, and the repair is an edit to the entity |
add .searchable() to a text() column of <entity>, then: x db gen "search <entity>" |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_SEARCH_IN_MEMORY |
the in-memory driver cannot answer a full-text match | a search run against memoryDriver(). REFUSED rather than emulated, and that is the whole decision: to_tsvector stems, drops stop words and applies a language's own rules, and websearch_to_tsquery parses quoted phrases and -negation — a JS token comparison is a DIFFERENT question with the same shape, so it would answer green in a unit test and differently in production, which is the one outcome the two-driver split exists to prevent. 500 for the same reason as the row above |
move this read into a <name>.live.test.ts and run it with TEST_DATABASE_URL set — bun test packages/entity/src/pg-search.live.test.ts is the model |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_STATE_UNDECLARED |
that column declares no state machine |
transition() on a column with no .transitions() — a declaration bug and not a caller's, which is why cause lists the columns that DO declare one: the repair is naming a different column or writing the table, and both are edits to source. 500, the same shelf as X_SEARCH_UNDECLARED
|
add .transitions(…) to <entity>.<column> — it must be an enumerated() column, and every value that set declares needs an entry. When other columns already declare a machine, the fix lists those instead |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_STATE_TRANSITION_ILLEGAL |
the machine has no such transition | three conditions and one code, because they share one repair — the move is not in the table — and cause states which. "<from>" is not a state the enumerated() set declares; the row is terminal in "<from>", an empty move list being how a terminal state is written; or the move to "<to>" was never declared. The unknown-state branch is separate because an unknown state has no outgoing moves either, so a single "no legal moves" answer reported a typo as "the row is terminal in <typo>" — a sentence about a state that does not exist. 422 and not 400: the request is well formed and its schema passed, which is X_INVARIANT_VIOLATED's shape and takes its status. Refused before any statement opens a connection, so nothing was written |
the fix is the same call with a legal target substituted — orders.transition('status', id, { from: 'pending', to: 'paid' }) — or add "<to>" to the "<from>" entry of the transitions() table |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_STATE_CONFLICT |
the row changed after the read this transition was decided on | the lost update, caught: two callers both read pending, both found the move legal, and the second one's statement carried status = 'pending' in its predicate and matched nothing. Or the row is still in from but a column the deciding policy read (move.observed, #702) changed — cause names the column, never its value. The state in cause is read back AFTER the refusal, so it is a diagnosis and never the decision — the decision was the statement, and it was atomic. 409 and not 422: nothing is wrong with the request, the identical one succeeds a moment later, and re-read-and-retry is precisely what a 409 tells a client to do |
re-read the row and decide again against the state cause names: orders.findById(id) — a transition names the state it expects, so a stale read is refused rather than overwritten |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_UNAVAILABLE |
cannot reach the database | nothing listening on DATABASE_URL, a closed pool, or @electric-sql/pglite not installed for a pglite:// url — every failure that never reached a server, and As of 2026-09-05 only those: a failure carrying a SQLSTATE is proof the server answered and is one of the two rows below. The driver's own words ride in the cause (ECONNREFUSED 127.0.0.1:5432). As of 2026-11-02 also a plain statement with no answer past the role's statement timeout + 5 s: the cause names the bound and the statement, the pool is replaced and the next statement opens a new one, and an autocommit write may have landed. Unclassified for retry, so a job worker re-runs the attempt: that write runs again, at least once |
set DATABASE_URL to a reachable Postgres, or x dev for the embedded PGlite — bun add @electric-sql/pglite when the url is pglite://
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_SCHEMA_STALE |
the statement names a table or column the database does not have | SQLSTATE 42P01 / 42703 — an entity edited before the migration that adds its column ran, on either driver (PGlite under x dev included). The cause leads with [SQLSTATE 42703] column "host_id" does not exist and carries the statement after it, excerpted, so the column list can never push the message off the line |
x db gen "<what changed>" && x db migrate — only the migrate half when the migration already exists |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_STATEMENT_FAILED |
the database refused the statement | any SQLSTATE the table does not classify — a syntax error, a data exception, a check or not-null violation. The server's verdict, so re-running it gives the same verdict, and the connection is not the problem. 25P03 is the exception to that reading: the server closed a transaction idle past idleInTransactionMs, and its fix is to do slow outbound work before opening the transaction |
read the SQLSTATE and message in the cause; psql "$DATABASE_URL" -c "<the statement>" reproduces it by hand |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_UNIQUE_VIOLATION |
a unique constraint rejected the row | SQLSTATE 23505 — two clicks racing a signup, or an insert where an upsert was meant. One of six driver failures that used to reach the caller as X_DB_UNAVAILABLE, whose fix reads "set DATABASE_URL to a reachable Postgres": a raced signup paged on-call for an outage that never happened. meta.constraint names the index the server refused on |
upsertAll(rows, { onConflict: [...] }) over the columns the constraint cause names — or catch X_DB_UNIQUE_VIOLATION and answer 409, which is what a raced signup is |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_FOREIGN_KEY_VIOLATION |
a foreign key constraint rejected the row | SQLSTATE 23503 — the parent row is absent, was deleted between the read and the write, or belongs to another tenant (a same-tenant key answers absent and foreign alike, and the memory driver refuses only the second). meta.constraint names the key |
insert the row the constraint cause names points at first, in the same withTransaction(...) — or drop the write, because the parent it names is gone or is not this tenant's |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_SERIALIZATION_FAILURE |
the transaction lost a serialization race | SQLSTATE 40001 or 40P01 — a serializable transaction whose read set another commit invalidated, or a deadlock the server broke. Retryable by definition, which is why it is its own code and not an outage |
withTransaction(fn, { retry: 3 }) — fn re-runs from the top, so it must be idempotent |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_STATEMENT_TIMEOUT |
the statement ran past its statement_timeout
|
SQLSTATE 57014 — a sequential scan where an index was assumed, or a plan that changed under a grown table. On the memory driver, a lock or lockKey waiter past 5 s: usually two locks taken in opposite order |
add the index this statement needs to the entity (indexes: [...]), then x db gen "add index"
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_LOCK_TIMEOUT |
the statement waited past its lock_timeout
|
SQLSTATE 55P03 — usually a DDL statement queued behind a long transaction holding the same table, which then blocks every reader behind it. |
x db diagnostics --json | jq '.data.blockers.rows[] | {pid, blockedBy, blockerState}' — end the blockedBy session or wait for it, then re-run the statement |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_POOL_EXHAUSTED |
no connection was available | SQLSTATE 53300 from the server, or the local pool answering nothing inside acquireTimeoutMs, for a plain statement as for a transaction (As of 2026-11-02) — deliberately one code, because to a caller both mean "there was no connection for this unit of work" and a second code would split one runbook in two. Refused rather than queued forever: a hang makes /readyz join the queue, the kubelet kills the pod, and the next pod inherits the same saturated database |
set DATABASE_POOL_MAX below max_connections / replicas (per-role default: POOL_PROFILES), or cut the replica count |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_DRAIN_TIMEOUT |
the pool did not drain inside its shutdown budget |
As of 2026-08-27: close() gave up waiting for a role's pool to hand its connections back. Bun.SQL's end() waits on an outstanding RESERVED connection and never stops — measured three runs per case on Bun 1.3.14 and 1.4.0, with the database perfectly healthy — so before PoolProfile.drainTimeoutMs a shutdown that met a pinned connection never finished, and the operator's only signal was a container that burned its whole termination grace period before SIGKILL. Raised on the elapsed time, because the driver RESOLVES when it gives up rather than rejecting: a drain that silently abandoned in-flight work looked exactly like a clean one. migrate and replicator are 0 and still wait forever, deliberately — a run-once role cutting off its own session mid-statement is worse than a slow exit |
find the statement that will not finish — psql "$DATABASE_URL" -c "select pid, state, query from pg_stat_activity where state <> 'idle'" — or raise drainTimeoutMs in postgresClient({ profile }) for the role cause names |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_MIGRATION_CONFLICT |
the migration ledger disagrees with this build | a ledger row from an app version this build does not ship, an applied migration whose file was edited so its checksum moved, or a rollback aimed at a row whose down SQL exists only in the build that shipped it. The wiki used to name x db status --json here; that command has never existed — the db subcommands are gen, migrate, reset, studio, branch and backfill — and this is one of the two errors most likely to fire during a real deploy. One narrow exception is not a conflict (As of 2026-10-05): when every unknown ledger row sorts after the newest migration the build ships, and the build has no migration left to apply, the migrator reads it as a rollback onto a newer build's schema — it applies nothing, logs ultimate migrate ledger ahead of build and returns the ids as ahead. Every other rollback state is still X_MIGRATION_CONFLICT: an unknown row between shipped migrations, a shipped migration this database never applied, or a build that ships no migrations at all |
the fix branches on which half disagreed. Foreign ledger row: deploy the app version cause names, or drop its row — psql "$DATABASE_URL" -c "delete from x_migrations where id = '<id>'". Moved checksum: git log -- packages/db/migrations/<id>.sql, then git checkout <rev> -- … the revision whose checksum the ledger names (a committed edit is what HEAD holds), never edit an applied migration, then x db gen "<change>". x db gen alone exits 0 and writes the edit into the schema dump. Unknown down: read the ledger with psql "$DATABASE_URL" -c "select id, app_version from x_migrations order by id desc limit 5" and roll back on the build that shipped it |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_MIGRATE_CONCURRENT |
another migrator holds the migration lock | two overlapping deploys, or a migrator wedged on a partition — or OOM-killed with its backend still alive — still holding pg_advisory_lock(<key>) when the wait ran out. pg_advisory_lock blocks with no timeout, so that case left helm upgrade --wait sitting inside one statement, printing nothing, with the job never failing so backoffLimit never fired. As of 2026-08 a bounded pg_try_advisory_lock poll turns it into an exit code |
psql "$DATABASE_URL" -c "select pid, application_name, state from pg_stat_activity join pg_locks using (pid) where locktype = 'advisory'" — pg_terminate_backend(pid) the wedged migrator, then x db migrate
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_MIGRATION_IRREVERSIBLE |
this migration cannot be reversed without data loss | a generated plan that drops a column or a table |
x db gen "<name>" --allow-destructive, or put the column back in columns and keep it |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_MIGRATION_DESTRUCTIVE |
this migration destroys data and does not say so |
As of 2026-08: a committed migration whose up drops a table, drops a column, truncates or retypes, with no -- destructive: true line in the file. Raised by x verify's drift step over the files, never by the migrator — the rail fails in CI, before a release phase applies it. Only up is judged: reversing a create table is a drop table, and marking every down marks nothing |
add the line -- destructive: true to the migration, or x db gen "<name>" --allow-destructive to regenerate it — the generator writes the marker itself. Add it before the migration is applied: the marker is SQL the checksum covers, so marking an applied file is an edit and answers X_MIGRATION_CONFLICT
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_MIGRATION_UNGENERATABLE |
this migration holds SQL no declaration carries and does not say so | the third rail of the drift step, and the one a squash discards. x db gen can only write what an entity declares, so a hand-written CREATE TYPE … AS ENUM, CREATE EXTENSION, GRANT, ALTER TABLE … REPLICA IDENTITY FULL or a data backfill is lost the moment anyone regenerates — and no declaration-based check can see it, because a regenerated sidecar equals the declaration by construction. examples/dummy's 0001_init.sql holds 7: five enum types and both replica identities. A count, not a boolean, so the first hand-written statement does not buy that file an unlimited allowance |
add the header line -- ungeneratable: <n> to the file packages/db/migrations/<id>.sql that the finding names, so a squash carries these statements by hand — or re-declare what an entity can express (an enum is a text column plus a check invariant) and run x db gen "<name>". Add the marker before the migration is applied: it is SQL the checksum covers, so marking an applied file answers X_MIGRATION_CONFLICT
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_MIGRATION_VIEW_DEPENDS |
a view is compiled against a column this migration retypes |
As of 2026-08-25: a preflight, run inside each migration's own transaction before its first statement, in both x db migrate and x db rollback. A view stores a compiled parse tree, not its text, so alter table … alter column … type under a dependent view is 0A000 cannot alter type of a column used by a view or rule and rolls the WHOLE migration back. The generator cannot see this by construction — a view is on no SchemaDescription, introspect() excludes views, and no entity can declare one — so the check lives where a connection exists. Until this code, the failure reached an operator as X_DB_UNAVAILABLE: cannot reach the database on a database the migrator was mid-transaction on, with the server's own rule _RETURN on view … depends on column sitting unread in a DETAIL field |
cause names the view, the table and the column; fix is a runnable pair of psql "$DATABASE_URL" -c '…' invocations — drop the view, then x db migrate, then re-create it from the create view the fix line carries verbatim, read out of pg_get_viewdef before the drop. Single-quoted deliberately: identifier() writes the view name in DOUBLE quotes, so the -c "…" form the sibling migration errors use would end the shell word at the name. A view whose name identifier() refuses (a quote, a space, a backslash — all legal inside a quoted Postgres name) gets a fix that names the view as JSON and asks you to quote it, rather than a fix: that throws |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_MIGRATION_SNAPSHOT_MISSING |
the newest migration records no schema snapshot |
As of 2026-08: x db gen found the newest migration on disk carrying no <id>.snapshot.json sidecar — deleted, or hand-written without one. Every generated migration writes the sidecar the next generation diffs against, so there is nothing to diff the entities against. Refused rather than defaulted to the empty schema, which would emit create table for every table the database already holds. A scaffold no longer starts in this state: x new writes no migration at all, and x db gen is the one writer of that directory. x doctor reports it too, As of 2026-08, so the condition is visible before a generate hits it |
git checkout -- packages/db/migrations/<id>.snapshot.json, or, when it was never written, delete the whole migration first and regenerate: rm packages/db/migrations/<id>.* && x db gen "<name>". That order is the fix — naming x db gen while the file is still there re-raises this code. The same condition reaches x db migrate as X_DB_DRIFT with an unknown-schema difference, whose fix: names the same two remedies in the same order |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_BRANCH_EXISTS |
that branch database already exists | two conditions, one code: x db branch create <name> for a name already taken — refused rather than overwritten, because a silent re-clone discards whatever the branch already held — or a dropBranch() aimed at the database this session is itself connected to, which Postgres cannot drop from underneath its own connection |
x db branch delete <name> # then re-create, or pick another name — x db branch list --json lists what exists. To drop the database you are connected to, reconnect elsewhere first: DATABASE_URL=.../postgres
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_SQL_UNSAFE |
SQL was built by string interpolation | a non-scalar interpolated into a sql template, or an identifier or branch name that is not [a-z0-9_-]+
|
pass a scalar (it becomes $n), a nested sql fragment, or wrap audited SQL in raw() — cause numbers the interpolation |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_REPO_CLIENT_PINNED |
a repository pinned to its own client cannot join the open transaction | a repository built through postgresDriver({ client }) used inside a transaction that was opened on another client. Refused rather than resolved: the pinned client takes a different connection out of its pool, so the write commits immediately and survives the rollback while the read cannot see rows the transaction already wrote — both silently. A transaction opened on the pinned client (withTransaction(fn, { client }), driver.transactor()) is joined, As of 2026-10-02 — tx.origin says it is the same database |
setDbClient(client) at boot and build the repository with no client: — postgresDriver() then resolves the open transaction through db() — or run this call outside withTransaction()
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_GEN_FAILED / X_DB_MIGRATE_FAILED
|
the x db step failed for a reason no framework error claimed |
a write the filesystem refused, the database unreachable. A failure the engine itself names — X_MIGRATION_CONFLICT, X_MIGRATION_IRREVERSIBLE, X_DB_UNAVAILABLE — reaches you as that code, with its own fix, never wrapped in these |
read cause — it carries the underlying error verbatim — then x doctor --json
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_BRANCH_FAILED |
x db branch refused, or failed for a reason no framework error claimed |
two shapes under one code. A deliberate refusal: delete aimed at a name x db branch list does not show, whose cause names the exact database or data directory it would have touched, so the refusal is checkable rather than assertable. Or an unclaimed failure of any of the three verbs — the clone connection refused, the data directory unwritable. X_BRANCH_EXISTS (the name is taken) and X_SQL_UNSAFE (the name is not [a-z0-9_-]+) reach you as themselves |
for the refusal, x db branch list --json — the name is not in the set, and only what that lists can be deleted. Otherwise read cause, then x doctor --json
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_STUDIO_FAILED |
reserved, not thrown — x db studio is planned and unreleased, and exits X_NOT_IMPLEMENTED. It shelled out to bunx drizzle-kit studio, a second schema engine for one subcommand |
— |
x dev, then the db panel at /_x
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_SCHEMA_DUMP_DRIFT |
the committed schema dump is not what the migrations produce | a file under packages/db/schema/ was edited by hand, a migration was written or changed without regenerating the dump, or the dump was never generated. The cause names the first file that differs; a dump that does not load back into the schema it was rendered from reports here too |
x db gen |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_ENTITY_SEALED_PREDICATE |
a sealed column is read by a predicate, an order or a rule the database evaluates | a .sealed() column in where, andWhere, orderBy, countBy, an aggregate, a filtered write's filter, an index or an invariant. The database holds ciphertext, so every comparison there is a comparison of ciphertexts: without lookup nothing matches, and with it only equality does — never an order, a range or like
|
edit the call the cause names — find the row by another column, or declare the column text().sealed({ lookup: true }) to allow equality (where / in / .unique()): equal plaintexts then store as visibly equal ciphertexts, so never for a low-entropy value |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_ENTITY_SEALED_IN_VIEW |
a sealed column is named in a view |
entity.$view([...]) lists a .sealed() column. A view is what leaves the server — an action's output, an OpenAPI schema, an MCP tool result — and a sealed column never does: $schema omits it for the same reason |
remove the sealed column from the $view([...]) list the cause names — server code reads it off the row; a caller that must be shown it gets its own field on a t.object output, set by the handler |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_TRANSACTION_ABORTED |
the server rolled the transaction back | a statement failed inside withTransaction and the body caught it instead of rethrowing; Postgres aborts the whole transaction on any statement error, so COMMIT answers ROLLBACK and nothing is stored |
await withTransaction(() => fallible()).catch(fallback) — a nested scope is a SAVEPOINT: only it rolls back — or rethrow the statement error |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_COMMIT_UNKNOWN |
the connection was lost while COMMIT was in flight | the socket closed after COMMIT was sent and before the server answered, so the transaction is either durable or rolled back and this process cannot tell which; neither onCommit effects nor onRollback undos ran. Only the data can say which: a row the transaction wrote is present if it committed and absent if it did not |
psql "$DATABASE_URL" — a session on that database: select a row the transaction wrote, and re-run the unit of work only when it is absent |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_SIBLING_SCOPE_TIMEOUT |
a nested transaction scope waited too long for its sibling | two withTransaction calls were started under one outer transaction and the first never finished inside siblingWaitMs — usually because its body awaits the second, which cannot start until the first returns |
await withTransaction(first); await withTransaction(second) — one after the other, never a nested body awaiting a sibling — or raise the wait: withTransaction(fn, { siblingWaitMs: 120000 }) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_ENTITY_APPEND_ONLY |
an append-only entity was asked to rewrite or remove a row | the call would rewrite or remove a stored row, and the entity is declared appendOnly: true |
repo.insert(row) — append the correction as a new row, or remove appendOnly: true from the entity and run x db gen |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_APPEND_ONLY_TRIGGER_MISSING |
an append-only table has lost its refusing trigger | the table is declared appendOnly, and its trigger ultimate_append_only is missing or disabled |
bun run x -- db migrate --json — after running the two create statements the finding's own fix prints, against that database |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_MIGRATION_APPEND_ONLY_BACKFILL |
a NOT NULL column added to an append-only table needs a default | existing rows could only get a value by an UPDATE the append-only trigger refuses |
entity('<name>', { columns: { <col>: <builder>.default(<value>) } }) — then re-run x db gen |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_ENTITY_OPERATOR_UNKNOWN |
a predicate names an operator outside the vocabulary | an operator that is not one of the closed set Operator declares — usually a SQL symbol ('>' for gt) or a different case ('GT'), reaching the plan through a cast, an operator built from a string, or a test run with no typecheck. Refused where the chain is written (andWhere) and again by both drivers for a plan built by hand, identically: before this the memory driver matched NO row with no error and the Postgres driver failed with a misleading X_SQL_UNSAFE. 500 — nothing the caller sends changes the answer, and the repair is an edit to source |
the fix opens with the call, the operator meant substituted — andWhere('likes', 'gt', value) # operators: eq, neq, … — and a placeholder with the whole list when nothing is close. SQL symbols map >→gt, >=→gte, <→lt, <=→lte, =→eq, !=/<>→neq; a known word in another case or with _ maps to itself ('GT', 'is_null'). The list is OPERATORS in @ultimat3/entity, and isOperator() tests a value read off the wire |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_MIGRATION_CONCURRENT_IN_TRANSACTION |
a concurrent index statement is in a migration that runs inside a transaction | a hand-written migration holds create index concurrently, drop index concurrently or reindex … concurrently but no -- transaction: false line, and Postgres refuses all three inside a transaction block (25001). Refused before the transaction opens, so nothing is applied |
sed -i '1i -- transaction: false' packages/db/migrations/<id>.sql — before it is applied — or move the statement into a migration of its own |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_DB_TRANSACTION_CLOSED |
statement on a finished transaction | a statement was sent on a transaction handle whose scope had already committed or rolled back: an un-awaited promise, or the loser of a Promise.all, outlived withTransaction | await it inside withTransaction(fn); for work that must outlive the scope, wrap it in outsideTransaction(fn) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_MIGRATION_ROLLING_UNSAFE |
this drop or rename would break the pods still running the old release | during a rolling deploy the old release keeps reading every column it declares, so a column dropped or renamed in the same release answers X_DB_SCHEMA_STALE on every read and insert of the old pods. Raised by x db gen for a drop not retired first, by the drift step (bounded by rollingSince in x.verify.json) over a committed migration, and by migrate() in production when a retire and its contract are both pending |
entity('<t>', { retired: ['<c>'] }), then x db gen "retire <c>", deploy, then remove it from retired and x db gen "drop <c>" --allow-destructive; one instance that stops before the next starts: x db gen "<name>" --allow-destructive --rolling-unsafe |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
X_LOCK_UNSUPPORTED |
a repository has no lock | db..lock or lockKey was called on a hand-written Repo that does not implement it
|
Ultimate — v31.9.0 As of 2026-10. The full-stack framework where the primary developer is an AI agent: bunx create-ultimate myapp → Getting started · live demo. MIT licensed. What npm serves is npm view @ultimat3/core version, never this line, and this footer is the only page that stamps a version.
Repository · Issues · Changelog · llms.txt
Edits to these pages are synced from wiki/ in the repository — change the file there, not the wiki, or the next sync overwrites it.
Start
- Getting started
- Installation
- Shapes
- Bare VM
- Project layout
- Migrating an existing app
- Compared to other stacks
Tutorials
- 1 · First app
- 2 · First feature
- 3 · Auth and admin
- 4 · Jobs and realtime
- 5 · Deploy free
- 6 · Growing up
Primitives
- The eight primitives
- Building your own base
- Actions
- Entities and migrations
- Policies and authz
- Queries and live queries
- Client data
- Jobs and workflows
- Scheduled tasks
- Routes and render modes
Capabilities
- Realtime
- Caching and invalidation
- Batching and preloading
- N+1 detection
- PWA and offline
- Client navigation
- MCP and AI
- Agents
- Admin dashboard
- Scraping
- Auth
- Notify
- Storage and uploads
- Feature flags
- SEO
- Static assets
Cross-cutting
- I18n
- Theming
- UI components
- Interface rules
- Timezones and dates
- Money
- Outbound HTTP
- Resource management
- Migrations and backfills
- Testing
- CI: the gate across parallel jobs
Reference