Consent Management
Description
Consent Management provides an immutable record of user consent states. To support high-performance lookups alongside a legally defensible audit trail, SCRIPE uses a dual-table architecture split between an append-only transaction ledger and a cached materialized view.
Consent Logging & Verification Flow
The consent lifecycle coordinates explicit opt-in, policy changes, and automated withdrawal cascades across connected systems.
Consent Purposes & Settings
Consent tracking is regulated by global profiles and structured consent purposes seeded at application startup:
| Purpose Key | Legal Basis | Mandatory | Sort Order | Active |
|---|---|---|---|---|
| essential | Contract | True | 1 | True |
| marketing | Consent | False | 2 | True |
| analytics | Consent | False | 3 | True |
RuleFor(x => x.RegulationProfileId).NotEmpty().WithMessage("Regulation profile ID is required.");
RuleFor(x => x.Key)
.NotEmpty().WithMessage("Purpose key is required.")
.MaximumLength(100).WithMessage("Purpose key must not exceed 100 characters.")
.Matches(@"^[a-z0-9_\.\-]+$").WithMessage("Purpose key must use lowercase letters, digits, underscores, dots or hyphens.");
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Purpose name is required.")
.MaximumLength(200).WithMessage("Purpose name must not exceed 200 characters.");
RuleFor(x => x.LegalBasis)
.NotEmpty().WithMessage("Legal basis is required.")
.MaximumLength(100).WithMessage("Legal basis must not exceed 100 characters.");
RuleFor(x => x.SortOrder)
.GreaterThanOrEqualTo(0).WithMessage("Sort order cannot be negative.");Dual-Table Database Architecture
To ensure both database performance and compliance audit integrity, consent tracking separates write-heavy transactions from read-heavy authorization checks:
public async Task UpsertAsync(ConsentSnapshot snapshot, CancellationToken ct = default)
{
var existing = await context.Set<ConsentSnapshot>()
.FirstOrDefaultAsync(s => s.SubjectId == snapshot.SubjectId
&& s.PurposeId == snapshot.PurposeId
&& s.TenantId == snapshot.TenantId, ct);
if (existing is null)
await context.Set<ConsentSnapshot>().AddAsync(snapshot, ct);
else
{
existing.CurrentAction = snapshot.CurrentAction;
existing.ConsentVersion = snapshot.ConsentVersion;
existing.RequiresReConsent = snapshot.RequiresReConsent;
existing.LastUpdatedAt = snapshot.LastUpdatedAt;
}
}Entity Reference
The following tables define the schema properties for both the append-only ledger and the materialized cache snapshots:
ConsentRecord (Append-Only Ledger)
| Field | Type | Description |
|---|---|---|
| Id | Guid | Unique identifier for the record. |
| TenantId | Guid | Foreign key referencing the tenant context. |
| SubjectId | Guid | Foreign key referencing the data subject (user). |
| PurposeId | Guid | Foreign key referencing the ConsentPurpose configuration. |
| Action | Enum (Granted/Withdrawn) | Consent action recorded (Granted or Withdrawn). |
| RecordedAt | DateTime | Timestamp representing when the ledger transaction occurred. |
| ConsentVersion | String | Consent policy document version active during submission. |
| IpAddress | String | Client IP address captured at the time of recording. |
| UserAgent | String | Browser user agent captured at the time of recording. |
| RegulationBasis | String | Regulatory context (GDPR, CCPA) active during submission. |
| CollectionMethod | String | Method used to collect consent (Web Form, Mobile App, API). |
ConsentSnapshot (Materialized Cache)
| Field | Type | Description |
|---|---|---|
| Id | Guid | Unique identifier for the record. |
| TenantId | Guid | Foreign key referencing the tenant context. |
| SubjectId | Guid | Foreign key referencing the data subject (user). |
| PurposeId | Guid | Foreign key referencing the ConsentPurpose configuration. |
| CurrentAction | Enum (Granted/Withdrawn) | Latest consent status cached for the subject and purpose. |
| LastUpdatedAt | DateTime | Timestamp representing the last snapshot modification. |
| RequiresReConsent | Boolean | Flag indicating the user must re-consent due to a policy version update. |
| ConsentVersion | String | Consent policy document version active during submission. |
Best Practices
Recommended Practices
- Verify the purpose key matches lowercase alphanumeric regex constraints.
- Always run the weekly ConsentExpiryJob to enforce re-consent on version updates.
- Consume MediatR ConsentWithdrawnEvents to restrict downstream data processing.
Anti-Patterns to Avoid
- Never bypass the append-only ledger by modifying ConsentRecord rows directly.
- Never execute direct database queries against ConsentRecord for frontend permission checks; always read ConsentSnapshot.
- Never expose raw unauthenticated consent recording endpoints.
API Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/v1/compliance/consent | Record an explicit user consent or refusal event with timestamp and IP evidence. | 🔒 Required |
| POST | /api/v1/compliance/consent/withdraw | Withdraw a previously granted consent | 🔒 Required |
| GET | /api/v1/compliance/consent/me | Retrieve the authenticated user's active consent selections across all purposes. | 🔒 Required |
| GET | /api/v1/compliance/consent | List tenant consent transaction records with purpose and subject filtering. | 🔒 Required |
| GET | /api/v1/compliance/consent/analytics | Retrieve aggregate consent adoption, opt-in rates, and purpose distribution. | 🔒 Required |