docs: RFC 0002 for explicit consent at signup - #1905
Conversation
# Conflicts: # web/sdk/client/views/pat/pat-details-view.tsx
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis RFC proposes explicit signup consent with configurable documents, flow intent propagation, consent validation, immutable consent records, audit events, and SDK checkbox support. It also defines errors, limitations, alternatives, references, and implementation order. ChangesExplicit Signup Consent
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: 🟡 Moderate · up to This draft changes signup/login behavior and consent records but still leaves a post-commit audit-loss path, potentially unsafe configured links, and conflicting or incomplete authentication and error-handling rules. These could cause missing compliance records or broken and misleading signup flows, so the PR needs fixes or explicit owner acceptance before merge. Suggested reviewers: 🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Coverage Report for CI Build 33112327895Coverage remained the same at 49.168%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
There was a problem hiding this comment.
Actionable comments posted: 6
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 1dc2fe21-4385-44c5-ac55-af0f437bbbc7
📒 Files selected for processing (1)
docs/rfcs/0002-explicit-consent-at-signup.md
Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.
| This also fixes something unrelated. `applyOIDC` never calls `consumeFlow`, so OIDC flow rows sit | ||
| around until the expiry cron while mail OTP rows are deleted on use. That is hard to justify once | ||
| those rows hold consent. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- repository convention files ---'
find /tmp/coderabbit-repo-knowledge/raystack-frontier-36c605d8 -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- RFC target ---'
sed -n '210,255p' docs/rfcs/0002-explicit-consent-at-signup.md
printf '%s\n' '--- passkey flow symbols ---'
rg -n -C 12 'finishPassKeyRegisterMethod|consumeFlow|PassKeyRegister' --glob '*.go' .Repository: raystack/frontier
Length of output: 12894
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- passkey registration implementation ---'
sed -n '504,632p' core/authenticate/service.go
printf '%s\n' '--- callback dispatch and user creation path ---'
sed -n '633,735p' core/authenticate/service.go
printf '%s\n' '--- flow repository delete contract ---'
rg -n -C 10 'type .*Flow.*Repository|func .*Delete\\(|DeleteExpiredFlows|flowRepo' core/authenticate --glob '*.go'
printf '%s\n' '--- scoped repository learnings ---'
cat /tmp/coderabbit-repo-knowledge/raystack-frontier-36c605d8/learnings/core.md
cat /tmp/coderabbit-repo-knowledge/raystack-frontier-36c605d8/learnings/repo-wide.mdRepository: raystack/frontier
Length of output: 6887
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- flow cleanup callers ---'
rg -n -C 8 'consumeFlow|applyPasskey|finishPassKeyRegisterMethod' core/authenticate --glob '*.go'
printf '%s\n' '--- applicable repository learnings ---'
sed -n '1,220p' /tmp/coderabbit-repo-knowledge/raystack-frontier-36c605d8/learnings/core.md
sed -n '1,220p' /tmp/coderabbit-repo-knowledge/raystack-frontier-36c605d8/learnings/repo-wide.mdRepository: raystack/frontier
Length of output: 12334
Apply flow cleanup to passkey registration too.
finishPassKeyRegisterMethod updates the user and returns without calling consumeFlow. The passkey callback path has no alternate cleanup owner, so successful registration leaves consent metadata until expiry. Consume the flow on success and terminal failure, or document and test another cleanup owner.
🧰 Tools
🪛 LanguageTool
[style] ~236-~236: To elevate your writing, try using a synonym here.
Context: ...il OTP rows are deleted on use. That is hard to justify once those rows hold consent...
(HARD_TO)
| `StartFlow` guesses signup from login for passkey by looking the user up | ||
| (`core/authenticate/service.go:220`), and `finishPassKeyLoginMethod` calls `getOrCreateUser`, so a | ||
| passkey login can create an account today. The intent replaces the guess: signup picks | ||
| `startPassKeyRegisterMethod`, login picks `startPassKeyLoginMethod`, and unspecified keeps the | ||
| guess so nothing existing breaks. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Define explicit passkey login behavior.
The RFC says that explicit FLOW_INTENT_LOGIN selects the passkey login path, but the table also calls a first-time passkey login a signup. Scope that statement to FLOW_INTENT_UNSPECIFIED only.
For FLOW_INTENT_LOGIN, finishPassKeyLoginMethod must return ErrLoginUserNotFound when no user exists. It must not create a user or write consent. Otherwise the stated login invariant remains false.
Also applies to: 354-359
| `getOrCreateUser` takes the flow, which is a signature change. The flow carries both the intent | ||
| and the consent, so one parameter serves both gates, and the nil-safe accessors mean | ||
| `authenticateWithPassthroughHeader`, which has no flow, passes nil and needs no branch of its own. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
printf '%s\n' '--- repository conventions ---'
find /tmp/coderabbit-repo-knowledge/raystack-frontier-36c605d8 -type f -name '*.md' -print
printf '%s\n' '--- scoped convention headers ---'
head -5 /tmp/coderabbit-repo-knowledge/raystack-frontier-36c605d8/*/*.md 2>/dev/null || true
printf '%s\n' '--- RFC lines 280-375 ---'
sed -n '280,375p' docs/rfcs/0002-explicit-consent-at-signup.md
printf '%s\n' '--- RFC symbols and related terms ---'
rg -n -C 3 'getOrCreateUser|authenticateWithPassthroughHeader|ResolveAll|unspecified|nil-safe|finishPassKeyRegisterMethod|consumeFlow|consent|intent' docs/rfcs/0002-explicit-consent-at-signup.mdRepository: raystack/frontier
Length of output: 38820
🏁 Script executed:
printf '%s\n' '--- RFC flow accessors and enforcement ---'
sed -n '215,265p' docs/rfcs/0002-explicit-consent-at-signup.md
printf '%s\n' '--- RFC implementation plan and limitations ---'
sed -n '588,670p' docs/rfcs/0002-explicit-consent-at-signup.md
printf '%s\n' '--- current authenticate implementation ---'
fd -t f -i 'service.go' | while read -r f; do
if rg -q 'getOrCreateUser|authenticateWithPassthroughHeader|finishPassKey' "$f"; then
printf '%s\n' "--- $f ---"
rg -n -C 5 'getOrCreateUser|authenticateWithPassthroughHeader|finishPassKey' "$f"
fi
doneRepository: raystack/frontier
Length of output: 11649
🏁 Script executed:
printf '%s\n' '--- complete current getOrCreateUser and nearby callers ---'
sed -n '780,845p' core/authenticate/service.go
printf '%s\n' '--- all current getOrCreateUser references ---'
rg -n -C 8 'getOrCreateUser|authenticateWithPassthroughHeader' core internal
printf '%s\n' '--- RFC references to passthrough and flow argument ---'
rg -n -C 10 'Passthrough|passthrough|takes the flow|flow presence|no flow|nil' docs/rfcs/0002-explicit-consent-at-signup.mdRepository: raystack/frontier
Length of output: 17044
Distinguish a nil flow from an unspecified-intent flow.
getOrCreateUser must skip consent enforcement only when flow == nil. The nil-safe accessors return FlowIntentUnspecified and no consent for both a nil flow and a non-nil flow with unspecified intent. If enforcement uses only those values, passthrough provisioning can be rejected, or an unspecified-intent signup can bypass ResolveAll. Add separate tests for both paths.
| They map to `CodeNotFound`, `CodeAlreadyExists` and `CodeFailedPrecondition`, returned from both | ||
| `Authenticate` and `AuthCallback`. `AuthCallback` maps a fixed list of errors to `InvalidArgument` | ||
| and everything else to `Internal` (`internal/api/v1beta1connect/authenticate.go:117`), so all | ||
| three have to be added to that list or they surface as 500s. `FailedPrecondition` is what lets a | ||
| client tell a consent rejection apart from a bad code or an expired flow and ask again. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- repository convention scopes ---'
head -5 /tmp/coderabbit-repo-knowledge/raystack-frontier-36c605d8/*/*.md 2>/dev/null || true
printf '%s\n' '--- changed RFC section ---'
sed -n '320,350p' docs/rfcs/0002-explicit-consent-at-signup.md
printf '%s\n' '--- related RFC section ---'
sed -n '510,535p' docs/rfcs/0002-explicit-consent-at-signup.md
printf '%s\n' '--- handler definitions and call sites ---'
rg -n -C 8 'AuthCallback|magicLinkHandler|CodeNotFound|CodeAlreadyExists|CodeFailedPrecondition|CodeInvalidArgument' internal docs/rfcs/0002-explicit-consent-at-signup.mdRepository: raystack/frontier
Length of output: 50374
🏁 Script executed:
#!/bin/bash
set -eu
log=/tmp/coderabbit-shell-logs/shell-output-UFZttL
printf '%s\n' '--- relevant convention files ---'
find /tmp/coderabbit-repo-knowledge/raystack-frontier-36c605d8 -type f -name '*.md' -print
printf '%s\n' '--- RFC excerpts ---'
sed -n '330,345p' docs/rfcs/0002-explicit-consent-at-signup.md
sed -n '520,532p' docs/rfcs/0002-explicit-consent-at-signup.md
printf '%s\n' '--- AuthCallback implementation ---'
sed -n '80,145p' internal/api/v1beta1connect/authenticate.go
printf '%s\n' '--- magicLinkHandler definitions and uses ---'
rg -n -C 12 'func .*magicLinkHandler|magicLinkHandler|AuthCallback' --glob '*.go' .Repository: raystack/frontier
Length of output: 50375
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- scoped repository learnings ---'
cat /tmp/coderabbit-repo-knowledge/raystack-frontier-36c605d8/learnings/internal-api-v1beta1connect.md
printf '%s\n' '--- exact frontend handler matches ---'
rg -n -C 15 'magicLinkHandler|status === 400|redirectSignup|redirectLogin' . --glob '!proto/v1beta1/frontier.pb.go'
printf '%s\n' '--- auth error declarations and service propagation ---'
rg -n -C 10 'ErrLoginUserNotFound|ErrSignupUserExists|ErrConsentRequired|FinishFlow|AuthCallback' internal core --glob '*.go'
printf '%s\n' '--- RFC surrounding references ---'
sed -n '300,345p' docs/rfcs/0002-explicit-consent-at-signup.md
sed -n '500,535p' docs/rfcs/0002-explicit-consent-at-signup.mdRepository: raystack/frontier
Length of output: 50375
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- Authenticate handler ---'
sed -n '1,100p' internal/api/v1beta1connect/authenticate.go
printf '%s\n' '--- authentication service error declarations and flow paths ---'
rg -n -C 8 'ErrLoginUserNotFound|ErrSignupUserExists|ErrConsentRequired|func \(s Service\) StartFlow|func \(s Service\) applyMailOTP|func \(s Service\) applyOIDC|func \(s Service\) getOrCreateUser' core/authenticate --glob '*.go'
printf '%s\n' '--- SDK client and dependency contracts ---'
rg -n -C 8 'HttpErrorResponse|createConnectTransport|create.*Client|FrontierServiceQueries|connect-web|connect-query' web/sdk web --glob 'package.json' --glob '*.ts' --glob '*.tsx'
printf '%s\n' '--- package versions ---'
find web -maxdepth 3 -type f \( -name 'package.json' -o -name 'pnpm-lock.yaml' -o -name 'yarn.lock' -o -name 'package-lock.json' \) -printRepository: raystack/frontier
Length of output: 50374
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- SDK package manifests ---'
find web -type f \( -name 'package.json' -o -name 'pnpm-lock.yaml' -o -name 'yarn.lock' -o -name 'package-lock.json' \) -print
printf '%s\n' '--- magic-link implementation ---'
sed -n '1,115p' web/sdk/client/views/auth/magic-link/magic-link-view.tsx
printf '%s\n' '--- SDK transport setup ---'
rg -n -C 10 'createConnectTransport|createGrpcWebTransport|createConnectQuery|TransportProvider|ConnectError|HttpErrorResponse' web/sdk --glob '*.ts' --glob '*.tsx'
printf '%s\n' '--- auth service relevant methods ---'
sed -n '230,370p' core/authenticate/service.go
printf '%s\n' '--- auth error declarations ---'
rg -n -C 3 'ErrLoginUserNotFound|ErrSignupUserExists|ErrConsentRequired' core internal --glob '*.go'Repository: raystack/frontier
Length of output: 50374
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- exact magic-link handler ---'
sed -n '1,110p' web/sdk/client/views/auth/magic-link/magic-link-view.tsx
printf '%s\n' '--- exact transport and error helper ---'
rg -l 'createConnectTransport|TransportProvider|function handleConnectError|const handleConnectError|HttpErrorResponse' web/sdk --glob '*.ts' --glob '*.tsx' \
| while read -r f; do echo "--- $f"; rg -n -C 12 'createConnectTransport|TransportProvider|function handleConnectError|const handleConnectError|HttpErrorResponse' "$f"; done
printf '%s\n' '--- tracked dependency manifests ---'
git ls-files | grep -E '(^|/)(package\.json|pnpm-lock\.yaml|yarn\.lock|package-lock\.json)$' | head -40
printf '%s\n' '--- Connect error handling conventions ---'
sed -n '1,180p' web/sdk/utils/error.ts 2>/dev/null || trueRepository: raystack/frontier
Length of output: 11052
Preserve the Connect error codes end to end.
AuthCallback maps every error in its fixed list to CodeInvalidArgument; errors outside the list become CodeInternal. Map ErrLoginUserNotFound, ErrSignupUserExists, and ErrConsentRequired to their intended codes instead of adding them to the existing branch.
MagicLinkView uses @connectrpc/connect-query, but its catch block checks err instanceof Response and status === 400. Handle the returned Connect error codes, including NotFound, AlreadyExists, and FailedPrecondition, or define one explicit transport mapping.
| The OIDC rejections arrive at `AuthCallback`, which is the callback page rather than the view that | ||
| started the flow, and that page has no error UI. Redirecting back to the originating view with an | ||
| error param is the smaller change, rendering it in place is the other option, and this is undecided. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Define the OIDC callback error path before implementation.
When the callback rejects an OIDC signup, the callback page has no error UI and the RFC leaves redirection undecided. The user has no documented way to return to the sign-up view and retry.
Specify the redirect target, an allowlisted non-sensitive error code, and how the originating view renders the error.
Shorter sentences and plainer wording throughout, and cut the paragraphs that restated a decision already made earlier in the document. No design changes: every decision, file reference and verified claim is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4f80a22e-3585-4be4-9eb5-ef22fb91b974
📒 Files selected for processing (1)
docs/rfcs/0002-explicit-consent-at-signup.md
Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review.
| Every document in the map is required at signup, so there is no per-document `required` flag. An | ||
| optional document would need withdrawal, which is out of scope. The cost is that adding a document | ||
| id breaks signup for clients still sending the old list, so a new document ships with the client | ||
| release that sends it. Version bumps are safe, since the client sends only ids. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Pin the document snapshot to the flow.
StartFlow stores only accepted_document_ids, but Grant later copies version and url from the current configuration. If configuration changes during an OIDC redirect, or an old client reaches a newer configuration, the immutable record can claim consent to a version the user did not see. This makes the consent evidence inaccurate. Store the resolved document snapshot or a configuration revision in Flow.Metadata, then make Grant use that snapshot. Remove the claim that version bumps are safe.
Proposed contract change
- "accepted_document_ids": ids,
+ "documents": resolvedDocuments,Also applies to: 184-189, 356-357, 519-522
| `enabled` switches the whole feature. Absent or false, frontier behaves as it does today, and | ||
| `accepted_document_ids` is ignored rather than rejected, so one client build works against both | ||
| kinds of deployment. True with no documents fails at boot rather than silently disabling itself. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make disabled-mode handling consistent.
When consent is disabled, the RFC says accepted_document_ids is ignored. It also says that login requests containing those IDs are rejected. A shared client can therefore fail login against a disabled deployment. Define which rule applies. If one client must work with both deployment modes, ignore the field while consent is disabled and apply the login rejection only when consent is enabled.
Also applies to: 134-136
|
Few points as we discussed -
|
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3f4716e0-66c8-40e4-a967-5a41169a19d1
📒 Files selected for processing (1)
docs/rfcs/0002-explicit-consent-at-signup.md
Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.
| `ListConsentDocuments`, unauthenticated, so a sign-up view can render the documents it is asking the | ||
| user to accept: | ||
|
|
||
| ```proto | ||
| message ConsentDocument { | ||
| string id = 1; | ||
| string title = 2; | ||
| string version = 3; | ||
| string url = 4; | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
set -eu
printf '%s\n' '--- applicable convention files ---'
find /tmp/coderabbit-repo-knowledge/raystack-frontier-36c605d8 -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- RFC structure and URL-related text ---'
ast-grep outline docs/rfcs/0002-explicit-consent-at-signup.md
rg -n -C 4 'URL|url|HTTPS|https|parse|scheme|ListConsentDocuments|ConsentDocument|SDK|link' docs/rfcs/0002-explicit-consent-at-signup.md
printf '%s\n' '--- SDK consent/document references ---'
rg -n -C 3 'ListConsentDocuments|ConsentDocument|consent.*document|document.*consent|href=|window\.location' web internal core api docs --glob '*.{ts,tsx,go,md,proto}' 2>/dev/null || trueRepository: raystack/frontier
Length of output: 50375
Security Misconfiguration (CWE-319): Cleartext Transmission of Sensitive Information
Reachability: External · Exploitability: Moderate
Allow only HTTPS document URLs.
The RFC accepts any parseable URL and exposes it through ListConsentDocuments for the SDK’s user-facing link. Require the https scheme during boot validation before publishing the document set.
Suggested validation contract
- URLs must parse
+ URLs must parse and use the https scheme| | `OrgID` | `schema.PlatformOrgID` | | ||
| | `IdempotencyKey` | empty. It is nullable, and there is nothing to deduplicate | |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
printf '%s\n' '--- repository conventions ---'
find /tmp/coderabbit-repo-knowledge/raystack-frontier-36c605d8 -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- RFC lines 420-475 ---'
sed -n '420,475p' docs/rfcs/0002-explicit-consent-at-signup.md
printf '%s\n' '--- audit and idempotency references ---'
rg -n -C 3 'Create|IdempotencyKey|consent_granted|audit|outbox|reconcil|deduplic' docs/rfcs/0002-explicit-consent-at-signup.mdRepository: raystack/frontier
Length of output: 12711
🏁 Script executed:
printf '%s\n' '--- relevant learning notes ---'
cat /tmp/coderabbit-repo-knowledge/raystack-frontier-36c605d8/learnings/internal-store-postgres.md
printf '%s\n' '--- transaction design ---'
sed -n '295,335p' docs/rfcs/0002-explicit-consent-at-signup.md
printf '%s\n' '--- audit definitions and repository calls ---'
rg -n -C 5 'type AuditRecord|AuditRecordRepository|func .*Create|IdempotencyKey|idempotency_key|CREATE TABLE.*audit|audit_records' --glob '*.go' --glob '*.sql' --glob '*.md' .Repository: raystack/frontier
Length of output: 50374
🏁 Script executed:
printf '%s\n' '--- audit-related files ---'
fd -i 'audit|auditrecord' . --type f | head -80
printf '%s\n' '--- exact audit repository declarations ---'
rg -l 'type .*Audit|AuditRecordRepository|IdempotencyKey|idempotency_key' --glob '*.go' --glob '*.sql' core internal pkg | head -80
printf '%s\n' '--- audit table/index definitions ---'
rg -n -C 4 'CREATE TABLE.*audit_records|CREATE UNIQUE INDEX.*audit|idempotency_key|target.*json|occurred_at' internal/store/postgres/migrations --glob '*.sql'Repository: raystack/frontier
Length of output: 11523
🏁 Script executed:
printf '%s\n' '--- postgres audit record repository ---'
sed -n '1,260p' internal/store/postgres/audit_record_repository.go
printf '%s\n' '--- audit record model and SQL mapping ---'
sed -n '1,260p' internal/store/postgres/audit_record.go
sed -n '1,220p' core/auditrecord/models/models.go
printf '%s\n' '--- audit repository interfaces and service ---'
sed -n '1,220p' core/auditrecord/auditrecord.go
sed -n '1,240p' core/auditrecord/service.go
printf '%s\n' '--- audit migration table and indexes ---'
sed -n '23,78p' internal/store/postgres/migrations/20250901054744_create_audits_table.up.sqlRepository: raystack/frontier
Length of output: 29445
Make the consent audit event durable and idempotent.
The post-commit AuditRecordRepository.Create call can fail after signup succeeds, leaving no user.consent_granted event. An empty IdempotencyKey maps to NULL, and the audit table permits multiple NULL keys. The non-unique target index does not prevent duplicates. Use a transactional outbox or a retry and reconciliation path keyed by the consent record ID.
Summary
app.consentconfig, the client sends the accepted document ids, and frontier writes an immutableuser_consentsrecord together with the new user in one transaction. Withapp.consentunset or disabled, nothing changes.flow_intentfield onAuthenticateRequestto separate login from signup. Consent is a signup-only requirement, and today both are the same request, so a login silently creates the account it should have refused and the consent check has nothing to key off before the OIDC redirect.usersso records outlive account deletion, andBEFORE UPDATE/BEFORE DELETEtriggers plus a create-only repository making them unmodifiable.user.consent_grantedaudit record fields, including why its actor has to be set explicitly rather than left to context enrichment on the skip-listed auth endpoints, and why it cannot be atomic with the consent write.