Skip to content

fix: unique request ids (UUIDv7) instead of "req-1, req-2…" - #33

Merged
BackStacked merged 1 commit into
mainfrom
claude/project-thread-e3mwmj
Nov 11, 2026
Merged

BackStacked merged 1 commit into
mainfrom
claude/project-thread-e3mwmj

Conversation

@BackStacked

Copy link
Copy Markdown
Contributor

Requested by Himanshu · project thread

Why

x-request-id: req-69 came from Fastify's default genReqId, a per-process counter. Graphql-yoga 6's useRequestId prefers the host's req.id and puts it on the response. So the id restarted at req-1 on every boot, repeated across the api replicas, and showed how many requests a replica had served.

What

  • src/request-id.ts: uuidv7() (RFC 9562: a 48-bit ms timestamp and 74 random bits, so it sorts by time) and requestIdGenerator(trustedHops), which is passed to Fastify as genReqId.
  • An incoming X-Request-ID is kept only through a trusted proxy (TRUST_PROXY ≥ 1) and only if it matches ^[A-Za-z0-9._:-]{8,128}$. Otherwise a new UUIDv7 is generated. Without a proxy the header is client input and is ignored, which keeps forged values and log injection out.
  • One id per request everywhere: Fastify logs it as reqId, Yoga reuses req.id for its logs and its response header, and an onRequest hook echoes it on every response, including /health and the 413 body-limit answer.
  • deploy/nginx.conf: proxy_set_header X-Request-ID $request_id; gives nginx and api logs the same id and overwrites anything the client sent. Envoy also sets one by default.
  • Docs (09-configuration.md, under TRUST_PROXY), llms-full.txt regenerated.
  • test/run.ts [config]: two new checks. GraphQL and /health get a different UUIDv7 on each request. The incoming id is kept only with TRUST_PROXY=1 and only when it is well-formed.

Testing

  • npm run kits could not run in the session: tag security-kit@v0.3.0 was not fetchable, and there is no Packages token. So the full npm test / typecheck is left to CI.
  • I tested the same wiring on a standalone Fastify 5 + graphql-yoga 6.0.1 app. It returned a fresh UUIDv7 on GraphQL and /health, kept a valid header only with a trusted hop, and replaced a malformed one. 100,000 ids came out unique and the timestamp encoding round-trips. A strict tsc (with noUncheckedIndexedAccess) passed on the module and the Fastify options.

Generated by Claude Code

…nter

x-request-id was Fastify's default genReqId ("req-1", "req-2", …): restarted on every
process, repeated across replicas, and revealed request volume. Each request now gets a
UUIDv7; behind a trusted proxy (TRUST_PROXY >= 1) a well-formed X-Request-ID from it is kept
so proxy and api logs share one id. Every response (GraphQL, /health, 413) echoes it, Fastify
logs it as reqId and Yoga reuses req.id for its own logs. nginx sets X-Request-ID $request_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jBohnJTiqYe47GyP9DsaG
@BackStacked BackStacked self-assigned this Nov 11, 2026
@BackStacked
BackStacked merged commit 42bff75 into main Nov 11, 2026
1 check passed
@BackStacked
BackStacked deleted the claude/project-thread-e3mwmj branch November 11, 2026 14:11
BackStacked added a commit that referenced this pull request Nov 11, 2026
<!-- claude-projects-attribution: {"github_login":"BackStacked"} -->
_Requested by **Himanshu** · [project
thread](https://claude.ai/code/project/chan_016JXeBZ2UxSi4N5VU2C26fQ?thread=cmsg_016JXeBZ2UxSi4N5VU2C26fQAJpeqaqNzFLLBxQs5TiAW8)_

This covers C1 and C2 from the handoff: a booking can be changed after
it is made, and it can be billed in the same request that creates it.

## updateBooking (`booking:update`)

A booking can be edited while it is booked, sample collected or in
process.

- **Doctor, collection type, time, address and notes.** Fields left out
stay as they are. `null` clears the doctor, the time or the address.
- **Add tests.** A new sample type gets a new pending tube.
- **Remove tests.** Only while the test has no results; its empty report
is dropped.
- A tube that hasn't been collected yet is dropped when no remaining
test needs it.
  - The last test can't be removed. Cancel the booking instead.
- **Billing follows, in the same transaction.**

| The invoice is | What happens | Extra permission |
|---|---|---|
| Unpaid | Cancelled and issued again with the booking's tests. Line
discounts and the bill discount (capped) are kept. | `invoice:create`,
plus `invoice:cancel` when tests are removed |
| Paid or credited | A credit note for each removed test's line.
`refundablePaise` says what can be refunded. | `accounts:post` |
| Paid, and tests are added | A new invoice for the added tests |
`invoice:create` |

- `Booking.invoice` stays on the first live invoice. The new
`Booking.invoices` lists every invoice of the booking, and
`cancelBooking` now checks all of them.
- Every edit is audited as `booking.update` with what changed.

Front desk can add tests, but can't remove a billed test, because that
needs `invoice:cancel` or `accounts:post`.

## createBooking `bill` (C2)

`bill: { payments, billDiscountPaise, applyTaxRules }` issues the
invoice for the booked tests inside the booking's transaction. A bad
discount or payment leaves no booking behind. It needs `invoice:create`,
plus `invoice:discount` for a discount and `payment:create` for
payments, the same as `issueInvoice`.

## Releases needed first

1. **client 1.10.0** (mgpl-diagnostics/client#17). `kits.json` already
points at it.
2. Then refresh the lockfile with `npm install
@mgpl-diagnostics/client@1.10.0`. Until then, CI's check that every
operation exists in the client fails on `updateBooking`.

No other kit changes: it uses db-kit 2.1.1 and finance-kit 1.0.1 as they
are. Version 0.1.4. Tag `v0.1.4` after merge; that release also carries
#33.

## Checks

- **PostgreSQL:** `TEST_BACKENDS=pg npm test`, with client 1.10.0
installed from its packed tarball, gives **260 passed, 0 failed**. That
includes 22 new end-to-end checks:
- billing at booking, including rollback on a bad payment and the
discount permission;
  - an unpaid invoice replaced;
  - a paid invoice credited, with a separate invoice for the added test;
  - the permission checks for front desk;
  - results locking a test in;
  - tenant isolation and the audit entries;
  - the trial balance still balancing.
- **MongoDB:** not run here, because this container can't download
MongoDB. CI runs it.
- Typecheck passes. The schema, API reference and llms files are
regenerated. `docs/07-domain-flows.md` and
`docs/examples/lab-day.graphql` are updated.

---
_Generated by [Claude
Code](https://claude.ai/code/session_018jBohjC7uBfVPopxyZBtS1)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants