Skip to content

Privacy Party (Controller & DPO)

GDPR Art. 13 §1 obliges every data controller to communicate to data subjects:

  • The identity and contact details of the controller (Art. 13 §1(a)) — mandatory.
  • The contact details of the Data Protection Officer, when one is designated (Art. 13 §1(b)).
  • The contact details of the competent supervisory authority (Art. 13 §2(d), Art. 77).

Hard-coding these in appsettings.json works but pushes a redeploy whenever the DPO changes role, the email rotates, or a new tenant onboarded with its own controller. Granit exposes them as runtime-editable settings so the values can be overridden globally and per tenant via the Granit.Settings admin API, and read by any template (notification emails, privacy policy page, document generation) through a {{ privacy }} global context.

Six settings are auto-registered by GranitPrivacyModule (no manual provider needed). All are visible to clients (IsVisibleToClients = true) and support the cascade Global → Tenant.

Setting nameGDPRRequired
Granit.Privacy.Controller.NameArt. 13 §1(a)yes
Granit.Privacy.Controller.EmailArt. 13 §1(a)yes
Granit.Privacy.Controller.PostalAddressArt. 13 §1(a)recommended
Granit.Privacy.Dpo.NameArt. 13 §1(b), Art. 37if DPO designated
Granit.Privacy.Dpo.EmailArt. 13 §1(b), Art. 38 §4if DPO designated
Granit.Privacy.SupervisoryAuthority.UrlArt. 13 §2(d), Art. 77recommended

The constants live in Granit.Privacy.Settings.PrivacySettingNames — reference them rather than duplicating the strings.

The configuration provider (C priority, between Default and Global) reads settings under the Settings section:

{
"Settings": {
"Granit.Privacy.Controller.Name": "Acme Corp SA",
"Granit.Privacy.Controller.Email": "[email protected]",
"Granit.Privacy.Controller.PostalAddress": "1 rue de la Loi, 1000 Bruxelles, Belgium",
"Granit.Privacy.Dpo.Name": "Jane Doe",
"Granit.Privacy.Dpo.Email": "[email protected]",
"Granit.Privacy.SupervisoryAuthority.Url": "https://www.autoriteprotectiondonnees.be"
}
}

Tenant administrators can override any of the values through the Granit.Settings admin API without a redeploy:

PUT /api/{version}/settings/Granit.Privacy.Dpo.Email
Content-Type: application/json
{
"value": "[email protected]",
"providerName": "T",
"providerKey": "<tenant-id>"
}

The cascade resolves Tenant → Global → Configuration → Default, so a missing tenant value falls back to the host-wide default automatically.

Templating: the {{ privacy }} global context

Section titled “Templating: the {{ privacy }} global context”

Granit.Privacy.Notifications registers a PrivacyContactGlobalContext that exposes the six settings as a {{ privacy }} namespace, available in every template the app renders — notification emails, document generation, user-defined templates served by Granit.Templating.

Available variables:

Template variableSource setting
{{ privacy.controller_name }}Granit.Privacy.Controller.Name
{{ privacy.controller_email }}Granit.Privacy.Controller.Email
{{ privacy.controller_postal_address }}Granit.Privacy.Controller.PostalAddress
{{ privacy.dpo_name }}Granit.Privacy.Dpo.Name
{{ privacy.dpo_email }}Granit.Privacy.Dpo.Email
{{ privacy.supervisory_authority_url }}Granit.Privacy.SupervisoryAuthority.Url

Missing settings render as empty strings — use Scriban’s truthiness checks to conditionally render the DPO block:

<p>
This message is sent by {{ privacy.controller_name }}
({{ privacy.controller_email }}).
</p>
{{ if privacy.dpo_email }}
<p>
For privacy-related questions, contact our Data Protection Officer:
<a href="mailto:{{ privacy.dpo_email }}">{{ privacy.dpo_email }}</a>.
</p>
{{ end }}
{{ if privacy.supervisory_authority_url }}
<p>
You may lodge a complaint with the supervisory authority:
<a href="{{ privacy.supervisory_authority_url }}">{{ privacy.supervisory_authority_url }}</a>.
</p>
{{ end }}

Typical use cases inside the framework:

  • Deletion confirmation (privacy.deletion_confirmed) — append a footer with controller + DPO contact so the user knows where to follow up.
  • Deletion reminder (privacy.deletion_reminder) — same footer, plus the supervisory authority link in case the user disputes the deletion.
  • Legal document obsolete (Privacy.LegalDocumentObsolete) — re-consent request signed by the controller.
  • Application-wide email layout — embed the contact in the standard footer so every transactional email (invoices, security alerts, password resets) carries a privacy contact line. Recommended for full Art. 13 transparency.

Regulation-aware deadlines ({{ model.response_deadline_days }})

Section titled “Regulation-aware deadlines ({{ model.response_deadline_days }})”

Templates that quote a legal response window — most prominently privacy.deletion_acknowledged — must NOT hard-code the deadline. GDPR Art. 12 §3 mandates one calendar month, but other regulations differ (CCPA: 45 days, LGPD: 15 days, PIPEDA: 30 days). Hard-coding “one month” in an English template and copy-pasting it into the Spanish/Portuguese variants would silently misrepresent the deadline for LGPD subjects.

The Granit.Privacy.Notifications package solves this by exposing the deadline as a model field rather than a literal in the template:

<p>
We will process your request within
<strong>{{ model.response_deadline_days }} days</strong>{{ if model.regulation }}
(<code>{{ model.regulation }}</code>){{ end }}.
</p>

The handler that publishes the notification resolves PrivacyDeletionAcknowledgedNotificationData.ResponseDeadlineDays by looking up the data subject’s regulation profile via IRegulationProfileRegistry (Granit.Privacy.Regulations). Each profile (GDPR, CCPA, LGPD, PIPEDA, …) declares its own statutory window, so the template renders the correct number of days for the recipient’s jurisdiction without code changes per locale.

When you author a new privacy notification template that quotes a deadline, follow this pattern: add a ResponseDeadlineDays field to the notification data record, populate it from IRegulationProfileRegistry in the Wolverine handler, and reference it as {{ model.response_deadline_days }} in every culture template. Doing so keeps the framework regulation-neutral and lets future regulations be added without touching template wording.

The context resolves all six settings in a single batched ISettingProvider.GetAllAsync(...) call per render. Values are cached by the settings layer (per-tenant), so cold reads cost one DB roundtrip per tenant and subsequent renders hit the cache.