Opt-Out (CCPA)
CCPA guest opt-out
Section titled “CCPA guest opt-out”The CCPA requires that users can opt out of data sale/sharing without creating an account or logging in. The opt-out system supports both authenticated users and anonymous visitors.
POST /privacy/opt-outis[AllowAnonymous]- If authenticated → opt-out recorded against
UserId - If anonymous →
AnonymousTrackIdgenerated, stored in_optout_idHTTP-Only cookie - Idempotent — repeated requests return existing opt-out status
Cookie _optout_id
Section titled “Cookie _optout_id”| Property | Value |
| -------- | ----- |
| Category | StrictlyNecessary (registered in ICookieRegistry) |
| HttpOnly | true |
| Secure | true |
| SameSite | Lax |
| Retention | 730 days (2 years) |
Cart merge (anonymous → authenticated)
Section titled “Cart merge (anonymous → authenticated)”When a visitor logs in after opting out anonymously, the application should merge the anonymous opt-out with their user profile:
// In your login handler (e.g., Wolverine handler on UserLoggedInEto)await optOutWriter.MergeAnonymousAsync(anonymousTrackId, userId);The framework provides the IOptOutRecordWriter.MergeAnonymousAsync() interface;
the merge call is application-level logic, not auto-triggered.
Endpoints
Section titled “Endpoints”| Method | Route | Operation | Auth |
| ------ | ----- | --------- | ---- |
| POST | /privacy/opt-out | RequestOptOut | Anonymous |
| GET | /privacy/opt-out/status | GetOptOutStatus | Anonymous |
Because POST /privacy/opt-out is anonymous, it is rate-limited via the
framework-owned policy privacy-optout-create (wire the body under
RateLimiting:Policies:privacy-optout-create; recommended: 5 requests/minute,
partitioned by client IP for guests and by user id when authenticated). Exceeding
it returns 429.
Provide a store backed by your own persistence and register it:
public sealed class OptOutRecordStore(AppDbContext db) : IOptOutRecordReader, IOptOutRecordWriter{ // IOptOutRecordReader public Task<OptOutRecord?> GetByUserAsync(Guid userId, CancellationToken ct = default) => /* … */; public Task<OptOutRecord?> GetByAnonymousTrackAsync(string trackId, CancellationToken ct = default) => /* … */; public Task<bool> IsOptedOutAsync(Guid? userId, string? anonymousTrackId, CancellationToken ct = default) => /* … */;
// IOptOutRecordWriter public Task RecordOptOutAsync(OptOutRecord record, CancellationToken ct = default) => /* … */; public Task RevokeOptOutAsync(Guid recordId, DateTimeOffset revokedAt, CancellationToken ct = default) => /* … */; public Task MergeAnonymousAsync(string anonymousTrackId, Guid userId, CancellationToken ct = default) => /* … */;}
services.AddGranitPrivacy(privacy => privacy.UseOptOutRecordStore<OptOutRecordStore>());See also
Section titled “See also”- Privacy overview — module setup and data provider registry
- Regulations — CCPA profile and per-tenant resolution
- Processing Purposes — categories tied to sale/share opt-out scopes
- Consent & GPC — GPC signal handling for anonymous opt-out
- Regulation Bridge — how cookies honour the CCPA opt-out flag