
Keep people’s processing choices explicit
A person agrees to optional processing one purpose at a time — analytics, marketing — and each answer stands on its own. Agreeing to one never implies another.
Nothing is assumed. A new person has granted nothing, and the moment they decide is recorded, so “declined everything” and “was never asked” stay different facts. The signed-in application asks once and waits for the answer; the marketing website asks each visitor before any analytics start.
Example — Respect an analytics choice
A visitor lands on the marketing site and declines analytics. The analytics client never loads, the choice is remembered on the next page, and a “privacy choices” link lets them change it later. After sign-up, the application asks the same question once; declining there is recorded with the value before and after.
For engineers
ConsentPurpose names the purposes. Each person stores the set they granted and the instant they decided. The set is audit evidence, so every change is recorded with its previous and resulting value.
Source: privacy.shared.schemas.ts (selected excerpt).
const consentedPurposesField = (subject: string) => z.array(z.enum(ConsentPurpose)).default([])
.describe(
`The optional purposes ${subject} has granted, each independently. A set, not a level: granting one purpose implies nothing about another. Empty means nothing optional was granted.`,
);
const consentDecidedAtField = (subject: string) => z.date().optional()
The decision instant is written by the server when the set changes, never taken from the caller, so a decision cannot be backdated. Use the preference resource operations — the application’s consent gate already does — so the audit emission records both values. Reset returns the set to empty and clears the instant: a withdrawal of everything, after which the person is asked again.
An application that must not be used without a purpose declares it once, as operatorComplianceIdentity.serviceConsentRequirement.requiredPurposes in wildo.saas.config.ts. wildo config service-consent carries it into the runtime: the backend refuses everything that person initiates — API and MCP operations, queued work, live-update subscriptions, agent tools, conversations and their results, charts and analytics — except their own profile and preferences, where they record the consent, and cancelling work they already started. Updates the server pushes on its own are not held back. Every signed-in page is withheld until they grant it. Signing in and out stay open, so the decision is always reachable. An empty set states that nothing is gated.
Use the inherited analytics consumer
Wildo’s mounted AnalyticsContextProvider starts the configured analytics providers only while the person has granted ANALYTICS, unless the application declares analytics.requiresConsent: false.
Selected source from AnalyticsContext.tsx, inside its configuration-and-consent effect:
const hasConsent = analyticsConfig.requiresConsent === false
|| hasGrantedConsentPurposes(grantedPurposes, [ConsentPurpose.ANALYTICS]);
if (!hasConsent) {
providersRef.current.forEach(p => p.reset());
setIsActive(false);
return;
}
On the marketing website the same rule is enforced where provider code starts: a provider whose capability rests on consent — product analytics — is neither loaded nor scripted until the visitor grants its purpose, and is stopped when they withdraw. Error monitoring and captcha start regardless, because they protect a service the visitor asked for. It does not recall events already sent. Other processing consumers still need their own declared purpose and enforcement.