> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stateset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API Hardening

> Fail-closed auth, durable idempotency, multi-tenancy rules, pagination limits, and per-route metrics — behavior changes that affect existing integrations.

# 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

<Warning>
  **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.
</Warning>

The explicit opt-out is a builder flag or the environment variable:

```bash theme={null}
STATESET_HTTP_ALLOW_UNAUTHENTICATED=true
```

An optional **strict authz mode** additionally denies unmapped `/api/v1` paths.

## Honest multi-tenancy

<Warning>
  A request carrying `x-tenant-id` against a deployment **without** tenant isolation now returns
  **400** instead of silently receiving shared data.
</Warning>

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`

| Situation                               | Response |
| --------------------------------------- | -------- |
| Key missing on a route that requires it | **428**  |
| Same key, different request fingerprint | **422**  |

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.

<Note>
  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.
</Note>

### 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`:

| Metric                                       | Type      |
| -------------------------------------------- | --------- |
| `stateset_http_requests_total`               | Counter   |
| `stateset_http_requests_errors_total{class}` | Counter   |
| `stateset_http_request_duration_seconds`     | Histogram |

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:

```bash theme={null}
STATESET_DB_MAX_CONNECTIONS=<n>
```

## 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.

## Related

* [Finance & Accounting](/stateset-icommerce/stateset-icommerce-finance)
* [Backup & Restore](/stateset-icommerce/stateset-icommerce-backup-restore)
