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
The explicit opt-out is a builder flag or the environment variable:/api/v1 paths.
Honest multi-tenancy
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 /ordersPOST /paymentsPOST /payments/{id}/refundPOST /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 useBEGIN 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 reportedisSupported() == 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.