Repository navigation
Checklist api backend
github-actions[bot] edited this page Nov 12, 2026
·
1 revision
Generated by
pnpm gen:checklist-statusfromdocs/qa/platform-checklist/areas/*.json. ⛔ Do not edit this page — edits go to the area JSON and this page follows on the next run. This page isChecklist-api-backend.
REST / batch / formula / build-time gates
33 active · 0 planned
| id | title | priority | status | personas |
|---|---|---|---|---|
api-backend.batch-transactional-discovery |
transactionalBatch capability bit matches the mounted /batch behavior — atomicity, rollback codes, size cap | P1 | active | admin (or any member entitled to the objects the batch touches) |
api-backend.formula-gates |
Formula runtime fixes hold and date-arithmetic fails at build time | P1 | active | — |
api-backend.enforce-or-remove-authoring-gates |
Removed/retired authoring keys fail at parse/build with located guidance | P1 | active | — |
api-backend.server-timing-admin-gated |
Server-Timing: the per-request X-OS-Debug-Timing path discloses spans to admins only, OS_SERVER_TIMING global mode to everyone, per-query detail admin-only always — on the server os dev actually runs | P2 | active | admin, non-admin member |
api-backend.query-contract-matrix |
Data-API query contract: every filter operator gives known answers; $-params, select, sort, expand honored; malformed input 400s with the exact code | P0 | active | admin (baseline known-answer runs), contributor (the RLS/FLS-restricted persona for the expand enforcement clause) |
api-backend.error-envelope-ledger |
Sampled endpoints return the standard error envelope with ledgered codes — no invented codes, no retired keys, statuses match the map | P1 | active | admin, a baseline member (for the 403 sample), anonymous (for the 401 sample) |
api-backend.bulk-write-contract |
Bulk write doors (createMany/updateMany/deleteMany, per-object batch): per-row outcomes, size cap, single/bulk parity, no silent full-table writes | P1 | active | a baseline member (bulk writes on showcase_private_note — also exercises the owner-scoping of #2982), admin (cap and parity runs) |
api-backend.route-ledger-live-parity |
Every route family across all ELEVEN route ledgers — REST, dispatcher, auth, storage/i18n/settings/datasource services, metadata HMR, trigger-api, cloud-connection, console static — is actually mounted on the live server — no route that exists only in unit tests | P2 | active | admin (authenticated, so a 401 cannot mask a 404) |
api-backend.declarative-endpoint-execution |
Metadata-authored api endpoints mount as real URLs with their declared auth/cache policy |
P1 | active | admin, anonymous |
api-backend.package-rest-lifecycle |
Package REST lifecycle: POST /packages creates (201), a duplicate name is refused 409 (no silent manifest clobber), PATCH partial-patches the manifest, and an explicit overwrite re-install replaces in place without duplicating | P2 | active | admin (manage_metadata / package authoring) |
api-backend.api-console-discovery-execute |
Developer API Console (a deep link on a stock boot): the endpoint tree mirrors /discovery, a seeded-object GET executes with live JSON + status + timing, and a malformed body surfaces the server error envelope verbatim | P2 | active | admin |
api-backend.filter-comparand-conformance |
Filter comparand conformance: only the accepted comparand TYPES pass the where-door, a bigint past the exact-integer limit is refused, and a dotted head is classified rather than guessed | P1 | active | seeded admin |
api-backend.date-range-preset-matrix |
Every date-range preset resolves to its declared macro window, and a bare preset name used as a raw comparand is refused with the prescription | P2 | active | seeded admin |
api-backend.retired-def-refusal |
A retired metadata DEFINITION (whole schema type withdrawn) refuses at every authoring door — distinct from a retired key, and the door that must not fall through to 'unknown type, ignored' | P1 | active | seeded admin, build-time author (no session) |
api-backend.packaged-action-disabled-dispatch |
A disabled packaged action is refused 409 ACTION_DISABLED at BOTH dispatch doors — after the capability gate (a 403 is never an oracle for the switch), before the param contract and the record load, durable across restart | P0 | active | admin (dev admin — holds manage_metadata for the activation flips via admin_full_access, and is the invoker on the 409 legs; the probe actions declare no requiredPermissions, so any authed caller clears the ADR-0066 D4 gate), any authenticated caller for the 403-oracle leg — showcase_zoo_perm_missing requires showcase.restricted_ops, a capability the app declares and grants to NOBODY (predicate-matrix.action.ts), so even the admin answers 403 there |
api-backend.action-activation-door-contract |
POST /actions/_activation/:object/:action — the activation door's ordered arms: authority before body and lookup, the exact 400 shapes, 404 undeclared, 409 ambiguity, 501/503 honesty, and the reserved segment surviving the real mount | P2 | active | admin (dev admin — manage_metadata via admin_full_access; the happy-path and body-arm caller), a non-admin member for the authority-first leg (Mei Phone phone.demo@example.com or Ada Auditor auditor.demo@example.com, DEMO_PERSONA_PASSWORD 'showcase123' — no showcase permission set grants manage_metadata, only admin_full_access carries it), anonymous (the 401 baseline control) |
api-backend.aggregate-contract-matrix |
Server-side aggregation contract: every AggregationFunction × DateGranularity gives known answers — ruled empty-set values, ISO-Monday week buckets, retired array_agg/string_agg refuse with prescriptions, and aggregate stays secured (engine-only, never raw driver) | P1 | active | admin (known-answer runs over the seeded baseline), a restricted non-admin persona for the secured-aggregate leg whose visible row set provably DIFFERS from the admin's: the search:qa-contributor-bound-member persona (a contributor-bound member who reads only its own showcase_invoice rows — give it one of its own, on an ACTIVE account). ⛔ Not the seeded demo personas: Mei Phone is refused showcase_invoice at the object gate (403 PERMISSION_DENIED, no answer to compare) and Ada Auditor holds viewAllRecords (her counts equal the admin's), so neither can tell a secured aggregate from a raw one |
api-backend.formula-stdlib-matrix |
CEL formula stdlib: all 30 bare-callable stdlib functions give known answers against the BUILT package — calendar-day timezone contract, addMonths month-end clamp, floor(-1.2)==-2, null-dropping joins, mixed int/double arithmetic, isoDate/isoDatetime text — and a formula calling an unregistered function is REFUSED at the authoring door (422), never stored to read null | P1 | active | build-time harness (no session — the matrix runs against the built @objectstack/formula package), admin (the live read-path parity leg) |
api-backend.api-methods-verb-gate |
enable.apiMethods three-state whitelist on the REST boundary: 404 hides apiEnabled:false, 405 OBJECT_API_METHOD_NOT_ALLOWED carries the EFFECTIVE derived set, bulk∧child + writeMode-precise import, legacy 8-value strip-and-WARN (deny-all cliff), the object gate answers ahead of the caller's 403, batch gated per-op before the txn | P1 | active | admin (proves the gate is OBJECT-level: even admin_full_access answers 405/404 on a narrowed object), a non-admin member for the ordering leg (Mei Phone phone.demo@example.com / Ada Auditor auditor.demo@example.com, DEMO_PERSONA_PASSWORD 'showcase123'), anonymous (the 401 baseline control) |
api-backend.rest-crud-config-contract |
RestServerConfig.crud is the CRUD surface's construction contract: five switches gate six mounts, dataPrefix moves the routes and their discovery advertisement together, and both tombstones refuse at construction | P2 | active | admin, authenticated — on the live-boot half a 401 must never be mistaken for an absent mount, no persona for the variant half: a non-default crud block is authorable only by a programmatic embedder (knownGaps), so those clauses are driven in a scratch harness, not by a signed-in user |
api-backend.rest-batch-config-contract |
RestServerConfig.batch is the bulk surface's construction contract: three switches and enableBatchEndpoint gate four per-object mounts (each ANDed with its protocol member), maxBatchSize is the one cap every bulk door measures, and both tombstones refuse at construction | P2 | active | admin, authenticated — the live-boot half, no persona for the variant half: a non-default batch block is authorable only by a programmatic embedder (knownGaps) |
api-backend.rest-metadata-config-contract |
RestServerConfig.metadata is the metadata surface's construction contract: prefix moves every metadata mount and their discovery advertisement, four endpoint switches each gate exactly the face its name states (#15542/#15854), maskObjectFields is the ADR-0106 D8 disclosure gate, and both tombstones refuse at construction | P1 | active | admin, authenticated — the live-boot half, a restricted member who cannot read every field of showcase_project — a member bound to the showcase_client_liaison position (showcase_client_liaison set: budget, budget_remaining and spent unreadable) — the masking clause is only observable as a non-admin, no persona for the variant half: a non-default metadata block is authorable only by a programmatic embedder (knownGaps) |
api-backend.rest-route-generation-tombstones |
RestServerConfig.routes is an all-tombstone sub-object: every one of the four retired selectors refuses at construction with the prescription that names where the contract moved, while the empty block still constructs | P2 | active | no persona — this is a construction-time and build-time contract, driven by an embedder authoring a config, not by a signed-in caller |
api-backend.second-object-exposure-and-internal-fields |
Every read that reaches a NON-addressed object asks that object's own enable block (nested-relation filter → 404/405, $expand withheld to the bare id, search skips it), and an internal: true field is refused 400 INVALID_FIELD in every evaluate position (filter, sort, groupBy, aggregate, $expand where/orderBy) |
P1 | active | seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot, org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as member, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password) |
api-backend.data-request-shape-and-query-arity |
Request-shape gates on v18: a /data write with a null / array / scalar body or an empty version token is refused 400 (never an unguarded 200), a repeated single-valued query name is refused 400 on the dispatcher doors (a doubled keepData no longer drops tables), and a stale If-Match on an unreadable row answers 404 | P1 | active | seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot, org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as member, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password) |
api-backend.meta-read-doors-v18 |
/meta read doors on v18: GET /meta/app/PACKAGE_ID returns that package's app, ?member= selects one bundle member on by-name doors, an unknown query parameter on any by-name door is refused 400, ?package=all folds to the no-parameter answer, and the object definition marks an FLS-denied field unsortable for that caller |
P2 | active | seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot, org member — created by the admin with POST /api/v1/auth/admin/create-user (stock showcase self-registration is closed: audience posture invite_only answers sign-up 403 SELF_REGISTRATION_CLOSED); joins the Default Organization as member, not platform admin, and lands mustChangePassword:true, i.e. a HELD session until it changes its password (POST /api/v1/auth/change-password) |
api-backend.error-sentences-and-unique-violation-wire-code |
Error answers made honest on v18: a hook refusal on a hand-built write route answers 400 with the hook's own sentence, a crash answers a generic 500 that withholds the caught message, and a unique-constraint refusal is 409 UNIQUE_VIOLATION with details at the action door, MCP and sandboxed bodies too | P2 | active | seeded admin (admin@objectos.ai / admin123) — the Default Organization owner and platform admin on a fresh boot |
api-backend.actions-namespace-address |
An action call names its holder: POST /api/v1/actions/:ns/:object/:action[/:recordId], /actions/:ns/global/:action and the _env forms run that namespace's (or the environment's) declaration, an action another package binds to an object is reachable by its namespace, and the bare call is unchanged | P1 | active | admin (the seeded admin): every call |
api-backend.meta-qualified-item-read |
A metadata item reads by its qualified address: GET /api/v1/meta/:type/:ns/:name and /meta/:type/_env/:name name the holder exactly, a bare read of a name two packages hold answers 409 naming both, and ?package= stays the exact qualifier | P1 | active | admin (the seeded admin): every read, a caller the bare item read refuses (no session): the parity leg |
api-backend.automation-namespace-address |
The automation, legacy trigger, connector connection and inbound hook doors take the namespace and _env segments: /automation/:ns/:name[/trigger|/toggle|/clone|/runs…], /automation/trigger/:ns/:name, /automation/connectors/:ns/:name/connection and /automation/hooks/:ns/:flowName/:hookId serve the name's one holder exactly as the bare path does, and refuse any other qualifier with 404, 409 RESOURCE_CONFLICT or, for the hook, the door's own 401 | P1 | active | admin (the seeded admin): every read, run and connection call, an external sender with no session: the inbound hook posts, a member without manage_metadata (check GET /api/v1/auth/me/permissions first): the gate leg |
api-backend.client-sdk-namespace-address |
The client SDK names an item's package: automation.*, actions.invoke / invokeGlobal and meta.getItem take an optional namespace, send the qualified path the server reads, send the bare path unchanged without one, refuse before sending any path the router would read as another request, and hand a 409's candidates to the caller | P2 | active | an integrator calling the platform through @objectstack/client, signed in as the seeded admin (admin@objectos.ai / admin123) with client.auth.login: every call |
api-backend.recycle-bin-delete-entry-restore |
A business-record delete keeps its rows in the recycle bin: the delete answer names the entry, GET /api/v1/data/:object/recycle-bin lists it with its cascade and retention, and POST …/recycle-bin/:entryId/restore writes every row back under its original id | P1 | active | seeded admin (admin@objectos.ai / admin123): every call |
api-backend.recycle-bin-refusals-purge-empty |
Recycle-bin refusals and removals: a detail whose master is in the bin or purged answers 409 naming why, purge and empty remove entries for good, the listing's paging is declared, and a non-manager or anonymous caller is refused | P1 | active | seeded admin (admin@objectos.ai / admin123): every positive call, a contributor-bound member (the search:qa-contributor-bound-member recipe): the non-manager probes, an anonymous caller (no session): the 401 probe |