Compliance
The problem
Section titled “The problem”Compliance is usually bolted on after the fact. A security audit reveals gaps, someone adds logging in a few places, a consultant writes a policy document, and the team moves on until the next audit.
Granit takes a different approach: compliance requirements are architectural constraints enforced by the framework itself. You cannot accidentally skip the audit trail because the interceptor runs on every write. You cannot forget tenant isolation because the query filter is applied globally. The framework makes the compliant path the default path.
GDPR — Articles 17, 18, and 25
Section titled “GDPR — Articles 17, 18, and 25”The General Data Protection Regulation requires data minimization, right to erasure, processing restriction, data portability, and privacy by design. Granit maps each requirement to a concrete framework mechanism.
Data minimization
Section titled “Data minimization”Each module stores only what it needs. There is no central “user profile” table with 40 columns — identity data lives in the IdP, cached fields are explicitly declared in UserCacheEntity, and modules reference users by ID only.
Right to erasure (Art. 17)
Section titled “Right to erasure (Art. 17)”Granit uses logical deletion, not physical deletion, through the ISoftDeletable interface and SoftDeleteInterceptor. When Delete() is called on a soft-deletable entity, the interceptor sets IsDeleted = true and DeletedAt / DeletedBy fields. The record stays in the database.
public interface ISoftDeletable{ bool IsDeleted { get; set; } DateTimeOffset? DeletedAt { get; set; } string? DeletedBy { get; set; }}A global query filter on ISoftDeletable excludes deleted records from all queries by default. When administrative access to deleted records is needed (audit, legal hold), the filter can be temporarily disabled:
public async Task<List<PatientResponse>> GetDeletedPatientsAsync( IDataFilter dataFilter, AppDbContext db, CancellationToken cancellationToken){ using (dataFilter.Disable<ISoftDeletable>()) { return await db.Patients .Where(p => p.IsDeleted) .Select(p => new PatientResponse(p.Id, p.FullName, p.DeletedAt)) .ToListAsync(cancellationToken) .ConfigureAwait(false); }}Processing restriction (Art. 18)
Section titled “Processing restriction (Art. 18)”The IProcessingRestrictable interface marks entities whose processing can be suspended on data subject request. Like ISoftDeletable, it has a corresponding global query filter. Restricted records are invisible to normal queries but remain in the database for legal compliance.
public interface IProcessingRestrictable{ bool IsProcessingRestricted { get; set; } DateTimeOffset? ProcessingRestrictedAt { get; set; } string? ProcessingRestrictedBy { get; set; }}Data portability
Section titled “Data portability”Granit.Privacy provides the IPersonalDataProvider interface. Each module that stores personal data registers a provider that can export its data in a structured format.
public class PatientPersonalDataProvider : IPersonalDataProvider{ public string Category => "Medical records";
public async Task<PersonalDataExport> ExportAsync( string userId, CancellationToken cancellationToken) { // Query all personal data for this user // Return structured export (JSON-serializable) }}The privacy module aggregates all registered providers to produce a complete data export for a given user — satisfying the right to data portability without each module knowing about the others.
Pseudonymization
Section titled “Pseudonymization”Two encryption strategies are available, both accessible through IStringEncryptionService:
- Granit.Encryption — AES-256-CBC with a local key. Suitable for development and non-production environments.
- Granit.Vault — Abstraction layer (
ITransitEncryptionService,IDatabaseCredentialProvider). Provider packages supply the implementations. - Granit.Vault.HashiCorp — HashiCorp Vault Transit engine. The encryption key never leaves Vault. Required for production under ISO 27001.
- Granit.Vault.Azure — Azure Key Vault RSA-OAEP-256 encryption. The encryption key never leaves Azure Key Vault. Alternative production backend for Azure-hosted environments.
public class PatientService(IStringEncryptionService encryption){ public async Task<string> StoreNationalIdAsync( string nationalId, CancellationToken cancellationToken) { string encrypted = await encryption .EncryptAsync(nationalId, cancellationToken) .ConfigureAwait(false); // Store encrypted value -- plaintext never persisted return encrypted; }}ISO 27001 — Information security
Section titled “ISO 27001 — Information security”Audit trail
Section titled “Audit trail”AuditedEntityInterceptor is registered globally through ApplyGranitConventions. Every entity that inherits from AuditedEntity gets four fields filled automatically on every write:
| Field | Set on | Source |
|---|---|---|
CreatedAt | Insert | IClock.Now (always UTC) |
CreatedBy | Insert | ICurrentUserService.UserId (or "system") |
ModifiedAt | Insert + Update | IClock.Now |
ModifiedBy | Insert + Update | ICurrentUserService.UserId (or "system") |
Application code never sets these fields. The interceptor runs inside the SaveChangesAsync pipeline, so it participates in the same transaction as the business write. Retention: 3 years minimum.
Encryption at rest
Section titled “Encryption at rest”IStringEncryptionService provides a unified API for encrypting sensitive fields before they reach the database. Two implementations:
| Implementation | Key management | Use case |
|---|---|---|
AesStringEncryptionService | Local config key (AES-256-CBC) | Development, testing |
VaultTransitEncryptionService | HashiCorp Vault Transit | Production (key never leaves Vault) |
AzureKeyVaultStringEncryptionProvider | Azure Key Vault RSA-OAEP-256 | Production on Azure (key never leaves Key Vault) |
The Vault module is conditionally loaded — it disables itself in Development environment, falling back to the local AES implementation automatically.
Encryption in transit
Section titled “Encryption in transit”Granit enforces HTTPS-only. The GranitJwtBearerModule sets RequireHttpsMetadata = true by default. There is no HTTP fallback configuration option in production — the option validator rejects it outside Development.
Tenant isolation
Section titled “Tenant isolation”Multi-tenancy provides three isolation levels, each mapping to a different ISO 27001 control:
| Strategy | Isolation | Data residency | Use case |
|---|---|---|---|
| SharedDatabase | Logical (query filter on TenantId) | Same database | Cost-effective, most deployments |
| SchemaPerTenant | Schema-level | Same server, separate schemas | Stronger isolation |
| DatabasePerTenant | Physical | Separate databases | Certification requirement, data sovereignty |
The IMultiTenant query filter is applied by ApplyGranitConventions — application code never writes WHERE TenantId = ....
Timeline — entity-level activity log
Section titled “Timeline — entity-level activity log”The Timeline module records entity-level events with author, timestamp, and a Markdown body. Unlike the audit trail (which captures field-level changes), the Timeline captures business events: “Invoice approved by Marie”, “Patient file transferred to Dr. Dupont”.
public async Task ApproveInvoiceAsync( Guid invoiceId, ITimelineWriter timelineWriter, CancellationToken cancellationToken){ // ... business logic ...
await timelineWriter.WriteAsync(new TimelineEntry { EntityType = "Invoice", EntityId = invoiceId.ToString(), Action = "Approved", Body = "Invoice approved for payment." }, cancellationToken).ConfigureAwait(false);}UTC enforcement
Section titled “UTC enforcement”Granit provides IClock (and integrates with .NET’s TimeProvider) as the sole source of time. IClock.Now always returns UTC. The framework never calls DateTime.Now or DateTime.UtcNow directly.
Compliance matrix
Section titled “Compliance matrix”Which Granit modules enforce which compliance requirements:
| Module | GDPR Art. 17 | GDPR Art. 18 | GDPR Art. 25 | ISO 27001 Audit | ISO 27001 Encryption |
|---|---|---|---|---|---|
| Granit.Persistence | Soft delete | Processing restriction | — | Audit interceptor | — |
| Granit.Privacy | Data export | — | Privacy by design | — | — |
| Granit.Encryption | — | — | Pseudonymization | — | AES-256-CBC |
| Granit.Vault | — | — | Pseudonymization | — | Abstraction layer |
| Granit.Vault.HashiCorp | — | — | Pseudonymization | — | Transit engine |
| Granit.Vault.Azure | — | — | Pseudonymization | — | Azure Key Vault RSA |
| Granit.MultiTenancy | — | — | Data isolation | Tenant isolation | — |
| Granit.Security | — | — | — | Actor attribution | — |
| Granit.Authorization | — | — | — | Permission audit | — |
| Granit.Observability | — | — | — | Structured logs | — |
| Granit.Timeline | — | — | — | Activity log | — |
| Granit.Workflow | — | — | — | Transition log | — |
| Granit.Templating | — | — | — | — | — |
| Granit.Timing | — | — | — | UTC enforcement | — |
Design decisions
Section titled “Design decisions”Why soft delete instead of physical delete? Audit retention. ISO 27001 requires a 3-year audit trail. If you physically delete a record, the audit entries referencing it become orphaned. Soft delete preserves referential integrity while hiding the record from normal queries. Physical deletion is available as an explicit administrative action for end-of-retention cleanup.
Why two encryption implementations? Development velocity. Developers should not need a running Vault instance to work locally. The IsEnabled check on GranitVaultModule automatically falls back to AES in development. In production, the Vault Transit engine provides key rotation, access logging, and HSM backing — none of which a local key can offer.
Why global query filters instead of repository-level checks? A missed WHERE clause is a data breach. Global filters on ISoftDeletable, IMultiTenant, IProcessingRestrictable, IActive, and IPublishable are applied by ApplyGranitConventions in OnModelCreating. You cannot forget them because you never write them.
Next steps
Section titled “Next steps”- Privacy reference — full API surface of
Granit.Privacy - Persistence reference —
AuditedEntity, interceptors, query filters - Security model concept — authentication and authorization architecture
- Multi-tenancy concept — tenant resolution and isolation strategies