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 userapi/src/fastify.d.ts: request-scoped activity logger entry type widenedapi/src/middleware/api-key-auth.ts: API-key context now carries no user idapi/src/services/quote-service.ts: removed the per-service author workaroundapi/src/services/invoice-service.ts: create accepts an absent authorapi/src/routes/contact-imports.ts: removed a placeholder-author fallbackapi/src/lib/request-context.ts: new assertion that a real user id is present on session-only routesapi/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.