Skip to main content

API Hardening

Several defaults changed in the 1.13โ€“1.14 series. Most are fail-closed, which means an integration that worked against a permissive earlier build may now be rejected. Read this page before upgrading.

Fail-closed API auth

A non-loopback bind now refuses to start without configured bearer auth. A deployment that previously listened on 0.0.0.0 with no auth will not boot.
The explicit opt-out is a builder flag or the environment variable:
An optional strict authz mode additionally denies unmapped /api/v1 paths.

Honest multi-tenancy

A request carrying x-tenant-id against a deployment without tenant isolation now returns 400 instead of silently receiving shared data.
This was a correctness fix: the old behavior looked like tenancy worked when it didnโ€™t. An escape hatch exists for proxy setups that always attach the header. Tenant ids reject dots, separators, and traversal sequences, and are checked with canonicalized path containment.

Durable idempotency

Idempotency-Key replays now survive restarts and work across replicas, backed by a database store on both backends with first-write-wins semantics and a TTL sweep. Keys are required by default on:
  • POST /orders
  • POST /payments
  • POST /payments/{id}/refund
  • POST /ap/payments
There is a builder and environment opt-out. All of this is documented in the OpenAPI spec.

Pagination and list limits

A server-side LIMIT policy applies to every list query: default 500, maximum 1000. Unbounded scans are no longer possible. Cursor pagination (after parameter, with next_cursor and has_more in the response) is available on both backends and both bindings for fixed assets, revenue contracts, cycle counts, work orders, purchase orders, quality inspections, and โ€” on PostgreSQL โ€” orders.
Hardcoded LIMIT 100 defaults on purchase orders, quality inspections, and NCRs were replaced with this shared policy, so those lists now return more rows by default than they used to.

List filters

Seven list-filter fields were previously accepted but silently ignored: date and total ranges on purchase orders, and date ranges plus reference, inspector, source, and SKU filters on quality inspections and NCRs (along with an NCR cursor bug, a PostgreSQL parameter-index bug, and an offset-not-zeroed-in-cursor-mode bug). All are now honored. If you passed these filters and worked around them client-side, that workaround is now double-filtering. purchaseOrders.list, workOrders.list, quality.listInspections, and quality.listNcrs accept optional filter and pagination arguments across the embedded accessor, both bindings, and the CLI tools. Zero-argument calls still work.

Per-route metrics

Request and error counts plus a real fixed-bucket Prometheus latency histogram, per (method, route), via middleware on MatchedPath: Exposed at /metrics.

Transaction discipline

All 99 SQLite write-transaction sites use BEGIN IMMEDIATE, eliminating deferred-upgrade deadlock and lost-update risk.

Query hygiene

N+1 item loading was batched across roughly 25 list paths on both backends, including BOM components, subscription plan and subscription items, promotion conditions, and cycle-count lines. Previously unbounded lists โ€” units of measure, exchange rates, tax jurisdictions and rates โ€” gained limit/offset under the 500/1000 policy.

Database configuration

PostgreSQL pool size is configurable:

Capability reporting fixes

Two domains previously reported isSupported() == true on SQLite while every call failed with no such table โ€” their tables existed only in PostgreSQL and in test fixtures. shipping_zones and search_config now have SQLite migrations, so the capability flag tells the truth.