v5.35.1 2 June 2026 Fix

API-Key Actor Correctness: Author Attribution and Activity Log

Why This Matters

A request authenticated by an API key has no human user behind it. The platform modelled that absent user with a placeholder id of zero, but zero is not a real user, and the columns that record who authored a record point at the user table. The result was a quiet correctness gap that only API-key traffic could hit: creating a record through the API could fail outright, and actions taken through the API were not always recorded in the activity log.

This release models the API-key actor honestly. There is no human user, so the recorded author is now empty rather than a placeholder, and that empty value is accepted everywhere the author is stored. Session (signed-in) behaviour is unchanged. The fix also removes a workaround that every future service would otherwise have had to copy.


Fixes

  • Record creation through an API key Creating a record via an API key stored a placeholder author that did not exist in the user table, so the write was rejected and the request failed. The author is now stored as empty for API-key requests, and creation succeeds.

  • Activity log for API-key actions The activity-log write used the same placeholder author and was rejected the same way. Because that write is best-effort, the failure was swallowed and the action simply went unrecorded. API-key actions now record correctly with an empty author.

  • Honest actor type at the auth boundary The authenticated-user id is now nullable from the auth layer through to storage, so the API-key context carries no human user rather than a placeholder. Routes that always run under a real signed-in session (admin and self-service account routes) are unchanged and are guarded by an explicit assertion that fails closed if a real user id is ever absent.


Operational Impact

  • Record creation via an API key now succeeds where it previously returned a server error.
  • Activity-log entries for API-key actions now persist with an empty author instead of being silently dropped.
  • No change to signed-in (session) behaviour: author attribution for logged-in users is exactly as before.
  • A per-service workaround is removed, so every service shares one correct path.

Versioning

Internal only. The authenticated-user id type was widened to allow an absent user. No public API contract changed: requests that previously failed now succeed, and successful requests are unaffected.

Enforced by the nullable author foreign keys and the nullable activity-log author column. Verified live by an API-key record creation storing an empty author and the matching activity-log entry recording with an empty author.


Files Changed

Backend:

  • api/src/server.ts: authenticated-user id type and activity-log entry type widened to allow an absent user
  • api/src/fastify.d.ts: request-scoped activity logger entry type widened
  • api/src/middleware/api-key-auth.ts: API-key context now carries no user id
  • api/src/services/quote-service.ts: removed the per-service author workaround
  • api/src/services/invoice-service.ts: create accepts an absent author
  • api/src/routes/contact-imports.ts: removed a placeholder-author fallback
  • api/src/lib/request-context.ts: new assertion that a real user id is present on session-only routes
  • api/src/routes/admin.ts, api/src/routes/admin-additions.ts: use the new assertion

Frontend / Docs site: None


Correct under both authentication paths, with one model of “who did this” shared across the whole surface.