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.

The consent lifecycle coordinates explicit opt-in, policy changes, and automated withdrawal cascades across connected systems.

Consent Submission — User updates preferences or submits a consent form.
ConsentExpiryJob — Weekly background job scans version mismatches and flags stale records for re-consent.
FluentValidation — Validates regulation constraints and purpose key syntax.
appends transaction if valid
Append Ledger — Writes an immutable ConsentRecord transaction containing IP address, user agent, version, and action.
updates current cache state from
Upsert Snapshot — Materializes current state in ConsentSnapshot cache for high-performance permission checks.
dispatches events on success
Domain Events — Publishes ConsentGrantedEvent or ConsentWithdrawnEvent via MediatR.

Consent tracking is regulated by global profiles and structured consent purposes seeded at application startup:

Purpose KeyLegal BasisMandatorySort OrderActive
essentialContractTrue1True
marketingConsentFalse2True
analyticsConsentFalse3True
AddConsentPurposeCommandValidator.csC#
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:

ConsentSnapshotRepository.csC#
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:

FieldTypeDescription
IdGuidUnique identifier for the record.
TenantIdGuidForeign key referencing the tenant context.
SubjectIdGuidForeign key referencing the data subject (user).
PurposeIdGuidForeign key referencing the ConsentPurpose configuration.
ActionEnum (Granted/Withdrawn)Consent action recorded (Granted or Withdrawn).
RecordedAtDateTimeTimestamp representing when the ledger transaction occurred.
ConsentVersionStringConsent policy document version active during submission.
IpAddressStringClient IP address captured at the time of recording.
UserAgentStringBrowser user agent captured at the time of recording.
RegulationBasisStringRegulatory context (GDPR, CCPA) active during submission.
CollectionMethodStringMethod used to collect consent (Web Form, Mobile App, API).
FieldTypeDescription
IdGuidUnique identifier for the record.
TenantIdGuidForeign key referencing the tenant context.
SubjectIdGuidForeign key referencing the data subject (user).
PurposeIdGuidForeign key referencing the ConsentPurpose configuration.
CurrentActionEnum (Granted/Withdrawn)Latest consent status cached for the subject and purpose.
LastUpdatedAtDateTimeTimestamp representing the last snapshot modification.
RequiresReConsentBooleanFlag indicating the user must re-consent due to a policy version update.
ConsentVersionStringConsent 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

MethodEndpointDescriptionAuth
POST/api/v1/compliance/consentRecord an explicit user consent or refusal event with timestamp and IP evidence.🔒 Required
POST/api/v1/compliance/consent/withdrawWithdraw a previously granted consent🔒 Required
GET/api/v1/compliance/consent/meRetrieve the authenticated user's active consent selections across all purposes.🔒 Required
GET/api/v1/compliance/consentList tenant consent transaction records with purpose and subject filtering.🔒 Required
GET/api/v1/compliance/consent/analyticsRetrieve aggregate consent adoption, opt-in rates, and purpose distribution.🔒 Required