Skip to content

Merge self-service profile writes server-side (#4) - #5

Closed
AntoineToussaint wants to merge 2 commits into
mainfrom
issue-4-updateuser-self-profile-write-has-a-read-modify
Closed

Merge self-service profile writes server-side (#4)#5
AntoineToussaint wants to merge 2 commits into
mainfrom
issue-4-updateuser-self-profile-write-has-a-read-modify

Conversation

@AntoineToussaint

Copy link
Copy Markdown
Contributor

Closes #4.

Summary

  • Self-service profile updates did a client read-modify-write and then sent the whole profile map, which the server replaced — so a concurrent write in that window was silently dropped (last-writer-wins).
  • Fixes it server-side with design 1 from the issue: business.UpdateUser's profile write now goes through a new store profile_merge path that does an atomic profile = (COALESCE(profile,'{}'::jsonb) || $set) - $emptyKeys. The write only touches the keys it sends (empty value clears a key), so it can't clobber a concurrent writer and no client read-modify-write is needed.
  • GDPR anonymization (gdpr.gostore.UpdateUser with the profile key) is left on the replace path, so a scrub still wipes every PII field — the compliance invariant the merge fix had to preserve.

Notes for the reviewer

  • This changes the UpdateUser RPC's profile semantics from replace to merge, as the issue's design 1 calls for. The GDPR path calls store.UpdateUser directly with the untouched profile (replace) key, so its behavior is unchanged.
  • The frontend updateSelfProfile / applyProfilePatch client code the issue references (module/services/frontend/code/src/features/user-profile/...) does not exist on this branch — it's part of unmerged work — so there is no client read-modify-write here to delete. Once that client lands it can send only the changed fields; the server is now ready for it.

Test plan

  • go test ./pkg/infra/ -run TestUpdateUserProfile — new tests against real Postgres:
    • merge preserves a concurrent writer's untouched keys, adds/overwrites sent keys, and clears keys sent empty
    • the profile replace path still wipes the whole map (guards the GDPR scrub invariant)
  • go build ./..., go vet ./pkg/infra/... ./pkg/business/...

AntoineToussaint and others added 2 commits July 24, 2026 14:54
Route business.UpdateUser's profile write through a new store-level
"profile_merge" path that atomically patches the JSONB map field-by-field
(empty value clears the key) instead of replacing it. Self-service writes
now send only the fields they change and can't clobber a concurrent writer.

GDPR anonymization keeps the "profile" replace path, so scrubbing still
wipes every field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The profile-merge fix edits two canonical base files (accounts users.go
and postgres_users.go) and adds a base test. Regenerate base-manifest.json
so base-integrity records the new canonical hashes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AntoineToussaint

Copy link
Copy Markdown
Contributor Author

CI status: blocked on the platform/agent bundle (PR #3), not on this change

The issue-#4 code fix is complete and verified — the profile_merge store test passes against real Postgres. The red Codefly CI check is a pre-existing platform blocker, reproduced locally with codefly v0.1.27:

  • verify / base-integrity — passes (base manifest refreshed for the two edited base files).
  • sync-drift — fails on saas-starter/accounts: agent explicitly does not support non-mutating sync.

Root cause: accounts (and auth-sidecar) pin go-grpc 0.1.11 via deployment/topology.bindings.codefly.yaml. That version has no downloadable CI release and explicitly declares no non-mutating-sync support. go-grpc 0.1.12 is published (linux_amd64) and sync-capable — but adopting it is a module-wide change (topology bump for two services + manifest regen) plus bumping ci.yml's CLI install pin v0.1.21 → v0.1.27 so infra sync-drift is skipped. That is the PR #3 bundle, not an issue-#4 concern.

main is red at the same sync-drift phase, so this PR is not a regression.

Plan: land PR #3, then rebase this PR and re-run CI. No change confined to this PR can make the gate green.

@AntoineToussaint

Copy link
Copy Markdown
Contributor Author

Closing as superseded — issue #4 is already fixed on main.

Commit a13a0ac ("fix: server-side profile merge to remove the self-update read-modify-write race", merged via #3) implements the same design this PR proposed: a dedicated profile_merge store op that atomically patches the JSONB profile field-by-field while GDPR anonymization keeps the full profile replace path. Main's version additionally locks the row with SELECT … FOR UPDATE, so it's a strict superset of this PR's inline-atomic approach, and it ships with an equivalent store test. Issue #4 was auto-closed (COMPLETED) when that landed.

Rebasing this branch would only conflict my implementation against the already-merged one for no behavioral gain. Nothing here is lost by closing.

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.

UpdateUser self-profile write has a read-modify-write race (server merge blocked by GDPR replace-semantics)

1 participant