Skip to content

Privacy — GDPR Data Minimization & Erasure

GDPR gives every user the right to request a full export of their personal data (Art. 15) and the right to be forgotten (Art. 17). In a modular application, personal data is scattered across multiple modules — patient records, uploaded documents, notification logs, audit trails. Implementing these rights manually means every module needs custom export/deletion logic, and missing one module means a compliance violation with fines up to 4% of annual turnover.

Granit.Privacy turns this into a framework concern: modules register as data providers, and a saga orchestrates collection or deletion across all of them. The saga handles timeouts, partial failures, and cascading notifications automatically. Legal agreement tracking (privacy policy versions, consent records) is built in, so you can prove compliance during an audit without building a custom consent system.

  • DirectoryGranit.Privacy GDPR data export saga, deletion with cooling-off, legal agreements
    • Granit.Privacy.Endpoints Minimal API endpoints for export, deletion, consent
    • Granit.Privacy.Notifications Deletion reminder and confirmation emails
    • Granit.Privacy.AI AI-powered PII detection in free-text fields
PackageRoleDepends on
Granit.PrivacyGDPR data export/deletion orchestration, legal agreementsGranit, Granit.BackgroundJobs
Granit.Privacy.EndpointsHTTP endpoints for GDPR Art. 7/15/17/20Granit.Privacy, Granit.Authorization, Granit.Validation
Granit.Privacy.NotificationsDeletion reminder and confirmation notification bridgeGranit.Privacy, Granit.Notifications
Granit.Privacy.AILLM-powered PII detection (IAIPiiDetector)Granit.Privacy, Granit.AI
[DependsOn(typeof(GranitPrivacyModule))]
public class AppModule : GranitModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddGranitPrivacy(privacy =>
{
privacy.RegisterDataProvider("PatientModule");
privacy.RegisterDataProvider("BlobStorageModule");
privacy.RegisterDocument(
"privacy-policy", "2.1", "Privacy Policy");
privacy.RegisterDocument(
"terms-of-service", "1.0", "Terms of Service");
});
}
}

Modules register themselves as data providers to participate in GDPR data export and deletion workflows:

privacy.RegisterDataProvider("PatientModule");

When a data subject requests export or deletion, the saga queries all registered providers and waits for each to complete.

stateDiagram-v2
    [*] --> Requested: PersonalDataRequestedEto
    Requested --> Collecting: Query all providers
    Collecting --> Prepared: PersonalDataPreparedEto
    Prepared --> Completed: ExportCompletedEto
    Collecting --> TimedOut: ExportTimedOutEvent
    Completed --> [*]
    TimedOut --> [*]

The export saga orchestrates data collection across all registered data providers. Each provider prepares its data independently, and the saga assembles the final export package.

Two modes are supported — immediate (default) and deferred with an opt-in cooling-off period:

flowchart TD
    A["POST /privacy/deletion"] --> B{defer?}
    B -->|false| C[PersonalDataDeletionRequestedEto]
    C --> D[Providers delete immediately]
    D --> E[DeletionExecutedEto]
    E --> F["Confirmation email"]
    B -->|true| G[DeletionDeferredEto]
    G --> H["GdprDeletionSaga starts"]
    H --> I["Schedule reminder"]
    H --> J["Schedule deadline"]
    I --> K["Reminder email"]
    J --> L{Cancelled?}
    L -->|No| M[PersonalDataDeletionRequestedEto]
    M --> N["Confirmation email"]
    L -->|Yes| O["Saga completed, data preserved"]

When the user sets defer: true, the deletion is postponed for a configurable grace period. During this period the user can cancel via POST /privacy/deletion/{requestId}/cancel. A reminder email is sent a few days before the deadline. A daily safety-net job (DeletionDeadlineEnforcerJob) catches any requests that the saga might have missed.

Configuration in appsettings.json:

{
"Privacy": {
"DefaultGracePeriodDays": 30,
"MaxGracePeriodDays": 90,
"ReminderDaysBefore": 3
}
}

Applications must provide a tracker implementation for deferred deletion state:

privacy.UseDeletionRequestTracker<EfCoreDeletionRequestTracker>();

Track consent to legal documents (privacy policy, terms of service):

privacy.RegisterDocument("privacy-policy", "2.1", "Privacy Policy");

Provide a store implementation to persist agreement records:

privacy.UseLegalAgreementStore<EfCoreLegalAgreementStore>();

ILegalAgreementChecker verifies if a user has signed the current version of a document — useful for middleware that blocks access until consent is renewed after a policy update.

Granit.Privacy.Endpoints exposes GDPR operations as a Minimal API surface. Mount the endpoints via MapGranitPrivacy():

RouteGroupBuilder api = app.MapGroup("api/v{version:apiVersion}");
api.MapGranitPrivacy(); // → /api/v1/privacy/...
MethodRouteOperationPermission
POST/exportRequestPrivacyExportPrivacy.Export.Execute
GET/export/{requestId}GetPrivacyExportStatusPrivacy.Export.Read
GET/exportListPrivacyExportsPrivacy.Export.Read

The POST endpoint triggers the scatter-gather saga. Each registered data provider prepares its fragment asynchronously. Poll the status endpoint or wire a Granit.Notifications handler on ExportCompletedEto to notify the user.

When the export is ready, ArchiveBlobReferenceId in the response references the archive blob. Use the BlobStorage download endpoint to obtain a pre-signed URL.

MethodRouteOperationPermission
POST/deletionRequestPrivacyDeletionPrivacy.Deletion.Execute
POST/deletion/{requestId}/cancelCancelPrivacyDeletionPrivacy.Deletion.Cancel
GET/deletion/{requestId}GetPrivacyDeletionStatusPrivacy.Deletion.Read
GET/deletionListPrivacyDeletionsPrivacy.Deletion.Read

When defer is false (default), publishes PersonalDataDeletionRequestedEto immediately. When defer is true, starts the GdprDeletionSaga with a cooling-off period. Each module handles its own data: physical delete, soft delete, anonymization, or retention with legal justification. A confirmation email is sent in both cases via DeletionExecutedEto.

MethodRouteOperationPermission
GET/agreements/documentsListPrivacyLegalDocumentsPrivacy.Agreements.Read
GET/agreements/statusGetPrivacyConsentStatusPrivacy.Agreements.Read
GET/agreements/historyListPrivacyAgreementHistoryPrivacy.Agreements.Read
POST/agreements/acceptAcceptPrivacyAgreementPrivacy.Agreements.Create

The accept endpoint validates that the submitted version matches the current version (rejects stale consent with 422), checks for duplicate acceptance (409), captures and pseudonymizes the client IP for the GDPR Art. 7 audit trail.

PermissionScope
Privacy.Export.ExecuteRequest personal data export
Privacy.Export.ReadView export status and history
Privacy.Deletion.ExecuteRequest, view, and cancel personal data deletion
Privacy.Agreements.ReadView legal documents and consent status
Privacy.Agreements.CreateAccept a legal agreement
{
"Privacy": {
"ExportTimeoutMinutes": 5,
"ExportMaxSizeMb": 100
}
}
CategoryKey typesPackage
ModuleGranitPrivacyModule, GranitPrivacyEndpointsModule
RegistryIDataProviderRegistry, ILegalDocumentRegistry, ILegalAgreementCheckerGranit.Privacy
BuilderGranitPrivacyBuilder, GranitPrivacyOptionsGranit.Privacy
Export trackerIExportRequestTrackerReader, IExportRequestTrackerWriter, ExportRequestStatusGranit.Privacy
EventsPersonalDataRequestedEto, PersonalDataDeletionRequestedEto, ExportCompletedEto, ExportTimedOutEventGranit.Privacy
EndpointsMapGranitPrivacy(), PrivacyEndpointsOptions, PrivacyPermissionsGranit.Privacy.Endpoints
ExtensionsAddGranitPrivacy()Granit.Privacy