Short answer: treat consent as a typed, expiring authorization record, and make account deletion revoke every session before any health-data worker can read again. A checkbox in a support portal is not a control; a revocation event that every verifier understands is.
I learned this during a customer-support deletion exercise. The request looked harmless: delete one account, close its tickets, and revoke every browser session. The account also had a health-data export pending in a queue. Our first design deleted the profile row and trusted the session cache to catch up. At 03:17, an old token still passed the API gateway while the queue consumer was processing the export. The support console showed a completed deletion because it only queried the profile database; the worker had already copied the export into its temporary object store, and the session cache would not evict that token until its normal five-minute sweep. No data left the system, but three independent clocks had to align for that outcome. The control was luck, not design.
The invariant is simple: a grant must name its category, scope, subject, issuer, and expiry; a revoke must be durable, ordered, and checked at each data boundary. This is slower than accepting a boolean consented flag. It is also what lets an incident responder answer the only useful question at 3am: which page fired, and why did this request still have authority?
What should category checks, grants, and revocation share?
Use one canonical decision object across interactive requests, background jobs, and support tooling. A health-data consent grant might allow lab_results:read for a named purpose until an expiry timestamp. Account deletion is broader: it creates a subject-level revoke that invalidates sessions, refresh tokens, queued work, and any derived cache entries.
Do not infer category from a URL or UI role. Store an explicit category such as health_data, then compare it with the resource classification at the point of access. A grant for appointment reminders cannot silently authorize raw lab results. Purpose and tenant belong in the comparison too, because a valid user session is not proof of a valid purpose.
The verifier should fail closed when the record is missing, expired, or revoked.
No exceptions. It should return a decision with a reason code, not merely false, so logs can distinguish category_mismatch from grant_expired and subject_revoked.
The deletion path is an incident boundary
Deletion needs a transactionally recorded intent and an idempotent event. Mark the subject as revoking first, publish subject.revoked, and make every consumer reject new work for that subject. Then remove or anonymize data according to the retention policy. If a downstream store cannot delete immediately, the authorization layer must still prevent reads while the erasure job completes.
Here is a deliberately small Go verifier. The interface is generic enough for a database, a signed ledger, or a service call; the important part is that every caller supplies the resource category and purpose instead of relying on ambient context.
package consent
import (
"errors"
"time"
)
type Grant struct {
Subject string
Category string
Scope string
Purpose string
Expires time.Time
Revoked bool
}
var ErrDenied = errors.New("consent denied")
func Authorize(g Grant, category, scope, purpose string, now time.Time) error {
if g.Revoked || g.Subject == "" || now.After(g.Expires) {
return ErrDenied
}
if g.Category != category || g.Scope != scope || g.Purpose != purpose {
return ErrDenied
}
return nil
}
In production, pair that check with a session version or revocation epoch. Every access token carries the version it was issued under; the verifier compares it with the subject's current value. Incrementing the epoch during deletion invalidates all sessions without chasing an unknown number of browser tokens. Queue consumers perform the same comparison immediately before reading health data, because authorization at enqueue time is stale by definition.
How do you test consent revocation when workers and sessions race?
Test the ordering, not just the happy path. Start a worker with a valid grant, revoke the subject, then release the worker's read step. The expected result is denial and an auditable reason, even if the job was queued before revocation. Repeat with duplicate revoke events, a replayed refresh token, a clock just past expiry, and a category mismatch.
Property-based tests are useful here: for any sequence containing revoke(subject), no later authorization decision for that subject may be allowed unless a new grant is issued under an explicitly permitted recovery policy. Keep the policy narrow. A support agent may confirm deletion status; that role should not resurrect health-data access.
Observability should expose decision reasons, category, purpose, token version, and event lag with redacted subject identifiers. Dashboards are hints. During an incident, a structured trail that says subject_revoked is more valuable than a green panel that only counts HTTP 200 responses.
Choosing boundaries that survive audits and outages
A single authorization service can simplify policy, but it also becomes a dependency in the deletion path. A signed, append-only consent ledger improves forensic review while making key rotation and replay protection your responsibility. Embedding checks in each service avoids a network hop, yet policy drift becomes likely.
The catch is operational: this pattern is not suitable when your team cannot provide durable event storage, clock monitoring, and a way to reprocess revocations. In that case, keep sensitive data behind a smaller policy boundary and choose a workflow with fewer asynchronous consumers. Stick with a simpler session-only model when the system never processes regulated categories; adding health-data semantics there creates ceremony without protection.
I am not sure any dashboard can prove erasure by itself. Your mileage may vary with retention laws and legal holds, so have counsel define which records must be anonymized, retained, or excluded before engineering encodes a deletion promise.
Top comments (0)