Skip to content

feat(api): record who created a document and who last wrote it - #142

Merged
hsinatfootprintai merged 1 commit into
mainfrom
feat/document-writer-attribution
Aug 16, 2026
Merged

feat(api): record who created a document and who last wrote it#142
hsinatfootprintai merged 1 commit into
mainfrom
feat/document-writer-attribution

Conversation

@hsinatfootprintai

Copy link
Copy Markdown
Contributor

FootprintAI/grandturks#1125 asks whether "user A created this document and user B updated it" is answerable. It wasn't: the metadata carried no writer at all, so a document overwritten by a different principal was indistinguishable from one written once — in content and in metadata alike.

Two fields, not one — and that is the point

Writing an existing documentId is an upsert, and there is no Update rpc. With only a creator, B's overwrite of A's document would still report A, and B's write would leave no trace anywhere.

field behaviour pairs with
_createdBy set on insert, never moves _createdAt
_updatedBy rewritten on every write _updatedAt

The asymmetry is enforced where it actually lives: updated_by is in DocumentCURD.Write's OnConflict DoUpdates list and created_by deliberately is not — exactly as with the two timestamps.

There's a test for that specifically, because if created_by ever joined that list, both fields would report the most recent writer, the pair would answer nothing, and every other assertion here would still pass. That's the failure mode worth guarding.

The resolver is injected, and optional

The writer comes from an injected CallerResolver, mirroring the existing project resolver: restcol is a library and doesn't own an authentication scheme, so only the wrapping service knows how a caller is represented.

Optional, unlike the project resolver. A deployment with no resolver keeps serving and records an empty writer rather than failing writes — attribution is worth having, not worth refusing data over, and this can be adopted without a flag day.

Empty is also honest: an unattributed document must stay distinguishable from one genuinely written by a principal named "system", so nothing invents a placeholder. Tested at both the storage and DTO layers.

Tests

The headline one walks the exact scenario from the issue — Alice creates, Bob overwrites — and asserts the creator survives, the updater moves, the payload really was replaced, and no second row appeared.

ok  github.com/footprintai/restcol/pkg/models/documents

Storage tests need postgres and run in CI. go build ./... and short-mode tests clean. (pkg/models/collections/collections.go shows as unformatted but was already so on main — deliberately not reformatted.)

What this does and doesn't finish

This closes the recording half of #1125. The other half is in grandturks: pipeline writes still reach restcol as one shared service account (storage_sinker.go's WithSaAuthInfo()), so once this lands they'd all be attributed to that single principal. A per-project identity for the sink is the remaining work, and this is its prerequisite — there was previously nowhere to put the answer.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BSzNfcVc1FnDCfk9AF68Eh

FootprintAI/grandturks#1125 asks whether "user A created this document
and user B updated it" is answerable. It was not: the metadata carried no
writer at all, so a document overwritten by a different principal was
indistinguishable from one written once - in content and in metadata
alike.

TWO FIELDS, NOT ONE, and that is the point. Writing an existing
documentId is an UPSERT and there is no Update rpc. With only a creator,
B's overwrite of A's document would still report A and B's write would
leave no trace anywhere.

  _createdBy  set on insert, never moves      pairs with _createdAt
  _updatedBy  rewritten on every write        pairs with _updatedAt

The asymmetry is enforced where it actually lives: updated_by is in
DocumentCURD.Write's OnConflict DoUpdates list and created_by is
deliberately NOT, exactly as with the two timestamps. There is a test for
that specifically, because if created_by ever joined the list BOTH fields
would report the most recent writer, the pair would answer nothing, and
every other assertion would still pass.

The writer comes from an injected CallerResolver, mirroring the existing
project resolver: restcol is a library and does not own an authentication
scheme, so only the wrapping service knows how a caller is represented.

OPTIONAL, unlike the project resolver. A deployment with no resolver
keeps serving and records an EMPTY writer rather than failing writes -
attribution is worth having, not worth refusing data over, and this can
be adopted without a flag day. Empty is also honest: an unattributed
document must stay distinguishable from one genuinely written by a
principal named "system", so nothing invents a placeholder. Tested at
both the storage and DTO layers.

Refs FootprintAI/grandturks#1125

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSzNfcVc1FnDCfk9AF68Eh
@hsinatfootprintai
hsinatfootprintai merged commit 06bd27a into main Aug 16, 2026
2 checks passed
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