Repository navigation
fix: unique request ids (UUIDv7) instead of "req-1, req-2…" - #33
Merged
Merged
Conversation
…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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Himanshu · project thread
Why
x-request-id: req-69came from Fastify's defaultgenReqId, a per-process counter. Graphql-yoga 6'suseRequestIdprefers the host'sreq.idand puts it on the response. So the id restarted atreq-1on 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) andrequestIdGenerator(trustedHops), which is passed to Fastify asgenReqId.X-Request-IDis 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.reqId, Yoga reusesreq.idfor its logs and its response header, and anonRequesthook echoes it on every response, including/healthand 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.09-configuration.md, underTRUST_PROXY),llms-full.txtregenerated.test/run.ts[config]: two new checks. GraphQL and/healthget a different UUIDv7 on each request. The incoming id is kept only withTRUST_PROXY=1and only when it is well-formed.Testing
npm run kitscould not run in the session: tagsecurity-kit@v0.3.0was not fetchable, and there is no Packages token. So the fullnpm test/typecheckis left to CI./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 stricttsc(withnoUncheckedIndexedAccess) passed on the module and the Fastify options.Generated by Claude Code