Legal Agreements
Why consent versioning?
Section titled “Why consent versioning?”When a legal document changes (privacy policy update, new ToS version), every user who accepted the old version must re-consent. GDPR Art. 7 requires proof of consent for each version. Granit tracks which version each user accepted and triggers re-consent flows automatically when a new version is published.
Static document registration
Section titled “Static document registration”For simple use cases, register documents at startup:
privacy.RegisterDocument("privacy-policy", "2.1", "Privacy Policy");privacy.RegisterDocument("terms-of-service", "1.0", "Terms of Service");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.
Runtime document management
Section titled “Runtime document management”For SaaS applications where administrators need to update legal documents without
redeployment, use Granit.Privacy.EntityFrameworkCore:
builder.AddGranitPrivacyEntityFrameworkCore(options => options.UseNpgsql(connectionString));This enables the LegalDocument entity — a VersionedWorkflowEntity with full
lifecycle management:
- Draft — being edited by an administrator
- Published — the active version users must consent to
- Archived — superseded by a newer version, preserved for audit trail
When a new version is published, the previous version is auto-archived in a
single transaction and LegalAgreementObsoleteEto is dispatched to trigger
re-consent notifications to all affected users.
Content rendering (soft-dependency on Templating)
Section titled “Content rendering (soft-dependency on Templating)”Set TemplateName on the document (e.g., "Legal.PrivacyPolicy") to render
multi-culture HTML content via Granit.Templating. If Templating is not loaded,
the document is file-only (DocumentBlobId via BlobStorage) or the application
handles rendering itself.
Distributed cache coherence
Section titled “Distributed cache coherence”The ILegalDocumentRegistry is backed by a ConcurrentDictionary cache. When a
document is published, LegalDocumentCacheInvalidatedEto is dispatched via
Wolverine so all pods refresh their cache — no stale versions served.
Consent endpoints
Section titled “Consent endpoints”| Method | Route | Operation | Permission |
|---|---|---|---|
| GET | /agreements/documents | ListPrivacyLegalDocuments | Privacy.Agreements.Read |
| GET | /agreements/status | GetPrivacyConsentStatus | Privacy.Agreements.Read |
| GET | /agreements/history | ListPrivacyAgreementHistory | Privacy.Agreements.Read |
| POST | /agreements/accept | AcceptPrivacyAgreement | Privacy.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 audit trail.
Admin endpoints
Section titled “Admin endpoints”| Method | Route | Operation | Permission |
|---|---|---|---|
| POST | /legal-documents | CreateLegalDocument | Privacy.LegalDocuments.Create |
| GET | /legal-documents/{id} | GetLegalDocument | Privacy.LegalDocuments.Read |
| GET | /legal-documents | ListLegalDocumentVersions | Privacy.LegalDocuments.Read |
| PUT | /legal-documents/{id} | UpdateLegalDocument | Privacy.LegalDocuments.Manage |
| POST | /legal-documents/{id}/publish | PublishLegalDocument | Privacy.LegalDocuments.Manage |
Publishing a draft auto-archives the previous published version and dispatches
LegalAgreementObsoleteEto. The notification handler streams affected users via
IAsyncEnumerable in batches of 1000 to avoid memory pressure on large tenants.
Integration events
Section titled “Integration events”| Event | Dispatched when | Consumer |
|---|---|---|
LegalAgreementAcceptedEto | User accepts a document version | Audit trail, cross-service sync |
LegalAgreementObsoleteEto | New version published, old archived | LegalDocumentObsoleteHandler → re-consent notifications |
LegalDocumentCacheInvalidatedEto | Document published or archived | All pods refresh ILegalDocumentRegistry cache |