Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions docs/api/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ This log records changes to the TrakRF public API under `/api/v1/` that affect i

Initial public API release. Stable contract for paths, field names, response shapes, and error envelopes per the [v1 stability commitment](./versioning).

### `id` is globally unique and opaque; `external_key` auto-mint clarified to `MAX(live key) + 1` {#id-global-uniqueness-external-key-max-plus-one}

Two identifier-model clarifications that sharpen the line between the surrogate `id` and the `external_key` handle. Pre-launch; no `v1.0.0`-or-later wire baseline to break. The `external_key` item is a docs correction only (service behavior unchanged); the `id` item reflects the move to a single shared id sequence landing under the hood.

- **The surrogate `id` is globally unique across every resource type, opaque, and permanent.** No two rows — of any type — share an `id`; an `id` never changes and is never reused, because there is no hard delete and the shared id sequence is never reseeded. The prior docs described ids as unique only within an entity type (so the same integer could be both an asset id and a tag id). That cross-type collision is eliminated: ids are now minted from one shared sequence, so global uniqueness holds by construction. Treat `id` as opaque (don't parse it, order by it, or infer a count or creation time) and use it as your durable foreign key. See [ID format](./id-format) and [Resource identifiers → Numeric `id` is a surrogate key](./resource-identifiers#numeric-id-is-a-surrogate-key).
- **`external_key` auto-mint is `MAX(live key) + 1`, not a lowest-unused-slot allocator.** Earlier wording ("lowest unused slot among live rows," "gap-filling") was incorrect: the server takes the highest live `ASSET-NNNN` / `LOC-NNN` in your org and adds one, zero-padded (`ASSET-%04d` / `LOC-%03d`). A middle-of-range delete therefore leaves a *permanent* gap that is never backfilled; only deleting the current highest live key frees its number for re-issue. A separate, caller-initiated path still lets you re-supply a soft-deleted key string directly (the partial unique index is `WHERE deleted_at IS NULL`, so deleted keys sit outside it). The [auto-mint section on Resource identifiers](./resource-identifiers#external_key-is-optional-on-create) and the `LOC-NNN` changelog entry below are corrected to match. Service behavior is unchanged; only the docs were wrong.

### Surrogate ids are int64 end-to-end; the int32 ceiling and id `too_large` are retired {#surrogate-ids-int64}

Every surrogate id — primary keys (`id`), foreign keys (`parent_id`, `location_id`, `asset_id`, `tag_id`, `org_id`), the `_id` query filters, and the numeric path parameters — now declares `format: int64` with `maximum: 9007199254740991` (JavaScript's `Number.MAX_SAFE_INTEGER`, 2⁵³−1) uniformly across response bodies, request bodies, query strings, and path parameters. The previous split — `format: int64` on the wire but an int32 (`2147483647`) runtime ceiling backed by an `int4` column — is gone: the storage column is now `int8`, and the spec is internally coherent at one id type everywhere. The id `too_large` rejection (`params.max: 2147483647`) is retired with it. Pre-launch alignment; no `v1.0.0`-or-later wire baseline to break.
Expand Down Expand Up @@ -39,9 +46,9 @@ Integrator authentication changes from "use your API-key JWT directly as a Beare

The location auto-mint format is `LOC-NNN` (3 digits, first mint `LOC-001` in a fresh organization), parallel to but intentionally narrower than `ASSET-NNNN`. The asymmetry reflects how the two resources are used in practice: assets accumulate ad-hoc creates without a pre-known partner-side handle (a quick scan-in from the SPA, a row pasted in from a CSV with no upstream SKU), while locations are typically named-and-known artifacts already documented in a facilities export, so auto-mint is the exception rather than the norm and the smaller slot suffices for the typical volume. Pre-launch correction; the [Resource identifiers](./resource-identifiers#external_key-is-optional-on-create) page previously described the location format as `LOC-NNNN` from the prior interim plumbing — no `v1.0.0`-or-later wire baseline to break.

- **Format and first mint.** Auto-mint produces `LOC-001` for the first omit-on-create in a fresh organization, then `LOC-002`, `LOC-003`, and so on, governed by the same "lowest unused slot among live rows" rule the [recycle paragraph on Resource identifiers](./resource-identifiers#external_key-is-optional-on-create) already documented for both resources. Soft-deleting the live row holding `LOC-005` makes that slot eligible for the next mint while soft-deleted rows can still hold the value; the partial unique index `(org_id, external_key) WHERE deleted_at IS NULL` is the system of record for "what slots are live."
- **Format and first mint.** Auto-mint produces `LOC-001` for the first omit-on-create in a fresh organization, then `LOC-002`, `LOC-003`, and so on, governed by the same `MAX(live key) + 1` rule the [auto-mint paragraph on Resource identifiers](./resource-identifiers#external_key-is-optional-on-create) documents for both resources. Because the next mint is one past the highest live key, soft-deleting a middle value like `LOC-002` leaves a permanent gap that is never backfilled, while soft-deleting the current highest `LOC-NNN` frees that number for re-issue; the partial unique index `(org_id, external_key) WHERE deleted_at IS NULL` is the system of record for "what keys are live."
- **Digit-count-agnostic past the natural width.** The `LOC-NNN` shape is the shape of the typical mint, not a width constraint on the value. Once the 3-digit space is exhausted in an organization the next mint is `LOC-1000`, then `LOC-1001`, with no migration or zero-pad reflow; the same property holds for `ASSET-NNNN` past `ASSET-9999`. Client-side parsers should match `^LOC-\d{3,}$` / `^ASSET-\d{4,}$` (or, more durably, treat the value as opaque) rather than anchoring on fixed-width digit groups. The new paragraph adjacent to the [auto-mint table](./resource-identifiers#external_key-is-optional-on-create) spells this out so a code generator emitting a strict-width validator doesn't reject a legitimately-minted `LOC-1042` after the 1000th create.
- **System-of-record guidance unchanged.** The load-bearing recommendation (supply the partner-side handle on create when integrating with an ERP / WMS / floor-plan tool — don't rely on the auto-mint) carries over verbatim; the auto-mint is the right call only for ad-hoc creates where no upstream handle yet exists. The recycle property and the namespace-shaped rather than counter-shaped wording from the [prior changelog entry on non-monotonic auto-mint](#bb52-docs--validation_error-vs-bad_request-envelope-split-non-monotonic-auto-mint-wording) both apply unchanged to the narrower location format.
- **System-of-record guidance unchanged.** The load-bearing recommendation (supply the partner-side handle on create when integrating with an ERP / WMS / floor-plan tool — don't rely on the auto-mint) carries over verbatim; the auto-mint is the right call only for ad-hoc creates where no upstream handle yet exists. The `MAX(live key) + 1` high-water-mark behavior and the non-monotonic, namespace-shaped (not counter-shaped) framing from the [prior changelog entry on non-monotonic auto-mint](#bb52-docs--validation_error-vs-bad_request-envelope-split-non-monotonic-auto-mint-wording) apply unchanged to the narrower location format.

### Tag rows cascade-soft-delete with their parent asset or location

Expand Down Expand Up @@ -149,7 +156,7 @@ Service relaxation that closes a recurring contract-class finding surfaced three
Two docs-discoverability items from the BB52 multipass triage. No spec or wire change.

- **[Errors → `validation_error` vs `bad_request`](./errors#validation_error-vs-bad_request) now explicitly warns typed clients against unconditional `fields[]` iteration.** The page already framed the parse-time vs validate-time split, but a handler shaped like `for f of body.error.fields { surface(f.field, f.message) }` silently swallows `bad_request` — the array is absent, the loop is a no-op, and the user sees an empty error toast for a real decoder-level failure (string-where-bool-expected, float-where-int-expected, JSON-integer-overflow). A new `:::warning` admonition spells out the failure mode and points to gating the iteration on `error.type === "validation_error"`. The narrative example near the type-mismatch block now also contrasts the JSON-integer-overflow case (`parent_id: 9999999999999999999999` → `bad_request`, no `fields[]`) against the in-range-but-too-large case (`parent_id: 2147483649` → `validation_error` with `code: too_large` and `params.max: 2147483647`) so an integrator reading the page sees both envelope shapes side-by-side before generating their error-handling code. Service behavior is unchanged; the contract and per-field codes already documented in [Errors → Validation errors](./errors#validation-errors) are load-bearing.
- **[Resource identifiers → `external_key` is optional on create](./resource-identifiers#external_key-is-optional-on-create) replaces "per-organization sequence" with non-monotonic wording.** The prior phrasing primed careful readers to expect Postgres-`nextval`-style monotonic allocation — once consumed, never reused. The observed behavior is "lowest unused per-organization slot in the `ASSET-NNNN` / `LOC-NNNN` namespace": deleting every live row holding `ASSET-0006` makes that slot eligible for the next mint even when soft-deleted rows still exist under the same natural key. The auto-mint section, the upstream natural-key summary, and the resource table column header all now use namespace-shaped language, and a new paragraph adjacent to the existing "available for reuse" sentence calls out the recycle property explicitly so partner-side audit logs aren't built on a false never-recycled-slot assumption. The load-bearing system-of-record guidance ("supply the partner-side handle on create — don't rely on the auto-mint") is unchanged; it now cites the recycle property as a second reason alongside the original "won't join cleanly to a SKU." Service behavior is unchanged.
- **[Resource identifiers → `external_key` is optional on create](./resource-identifiers#external_key-is-optional-on-create) replaces "per-organization sequence" with non-monotonic wording.** The prior phrasing primed careful readers to expect Postgres-`nextval`-style monotonic allocation — once consumed, never reused. The actual behavior is `MAX(live key) + 1` over the per-organization `ASSET-NNNN` / `LOC-NNN` namespace: deleting the current highest live `ASSET-0006` frees that number for re-issue on the next mint, while deleting a lower value leaves a permanent gap that is never backfilled. The auto-mint section, the upstream natural-key summary, and the resource table column header all use namespace-shaped (not counter-shaped) language, and a paragraph adjacent to the existing "available for reuse" sentence calls out that an auto-minted number is not a stable reference, so partner-side audit logs aren't built on a false never-re-issued assumption. The load-bearing system-of-record guidance ("supply the partner-side handle on create — don't rely on the auto-mint") is unchanged; it now cites the re-issue property as a second reason alongside the original "won't join cleanly to a SKU." Service behavior is unchanged.

### BB47-49 docs sweep — `parent_external_key` writability alignment, quickstart idempotence boundary, `openapi-fetch` PATCH CT surfaced on API clients page

Expand Down
6 changes: 5 additions & 1 deletion docs/api/id-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ The declared `maximum` is the **JS-safe-integer cap** (2⁵³−1), not a storag

## Why int64, and why the 2⁵³−1 cap

The TrakRF id namespace is randomly distributed across a wide range, not monotonically assigned from `1`. Declaring `int64` from v1 launch means a future widening of the id space is a non-event for typed clients — they are already typed wide enough:
The surrogate `id` is a high-entropy, server-assigned integer — **globally unique across every resource type** (no two rows, of any type, share an `id`), **opaque** (don't parse it, order by it, or infer a count or creation time from it), and **permanent** (it never changes and is never reused — there is no hard delete, and the shared id sequence is never reseeded). It is scattered across a wide range rather than monotonically assigned from `1`. Declaring `int64` from v1 launch means a future widening of the id space is a non-event for typed clients — they are already typed wide enough:

- Java / Kotlin clients surface ids as `Long`.
- C# clients surface `long`.
- TypeScript clients hold ids in `number`, exact up to 2⁵³−1 — exactly the declared `maximum`, so a value the spec admits is always a value the client can represent.

The `maximum: 9007199254740991` ceiling is the load-bearing half of that promise on the JS side: it guarantees the spec never admits an id a `number`-typed client would round. A client that synthesizes ids on its own side (uncommon — ids are server-assigned) should stay within the same bound.

:::note `id` is not `external_key`
This page is about the surrogate `id`. The string `external_key` (`ASSET-NNNN` / `LOC-NNN`) is a separate, per-organization handle with its own allocation — dense and low-valued, minted as `MAX(live key) + 1`. A dense or low-numbered `external_key` implies nothing about `id`; the two are allocated independently. See [Resource identifiers → `external_key` is optional on create](./resource-identifiers#external_key-is-optional-on-create).
:::

## Error envelopes at the id boundaries

Surrogate ids no longer carry a dedicated "too large" rejection. A syntactically valid id that doesn't resolve takes the ordinary lookup path for its surface:
Expand Down
Loading
Loading