DEV Community

BrantLockwood468
BrantLockwood468

Posted on

How to Build a Privacy Preference Center: Consent States for Gaming Accounts

Deleting a game account is the easy button. The hard part is building a privacy preference center that handles consent listing, granting, and revoking by category while the player is still signed in on three devices.

Short answer: model each consent action as a validated, auditable, recoverable state transition, then revoke sessions only after the current state says processing must stop.

Start with a before-and-after state model

Before a privacy preference center, a checkbox often drives a boolean in a profile table. That is too thin for GDPR work. A category needs a purpose, a trigger, an actor, and a timestamp. The transition should be explicit: unknown -> granted, granted -> revoked, or unknown -> denied. A delete request can then consume those states instead of guessing from the UI.

Think of the flow as a small diagram in words: player opens preferences, service reads current consent, player chooses a category, service records the transition, downstream jobs re-check it, and session revocation closes the remaining access paths. The screen is only a projection of that ledger.

Three words matter: validate, audit, recover.

For recovery, keep an immutable event with category and purpose, while maintaining a current-state view for fast checks. If a retry arrives after a network timeout, the same transition identifier should produce the same result. That prevents a double grant or a confusing revoke/grant ordering.

How should a privacy preference center list, grant, and revoke consent by category?

The API sequence is deliberately boring. Read first. Decide second. Write third. A minimal TypeScript client can make that order impossible to skip:

type Consent = { category: string; purpose: string; status: "granted" | "revoked" | "denied"; updated_at: string };

const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");

async function request(path: string, method: "GET" | "POST", body?: unknown): Promise<unknown> {
  for (let attempt = 0; attempt < 4; attempt++) {
    const response = await fetch(`${baseUrl}${path}`, {
      method,
      headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
      body: body === undefined ? undefined : JSON.stringify(body),
    });
    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("Retry-After") ?? "1");
      await new Promise((resolve) => setTimeout(resolve, Math.min(retryAfter * 2 ** attempt, 30) * 1000));
      continue;
    }
    if (!response.ok) throw new Error(`Consent request failed (${response.status}): ${await response.text()}`);
    return response.json();
  }
  throw new Error("Consent request exceeded retry limit");
}

async function setConsent(userId: string, category: string, action: "grant" | "revoke") {
  const listPath = "/v1/auth/consent/list_for_user/{user_id}".replace("{user_id}", encodeURIComponent(userId));
  const current = await request(listPath, "GET") as Consent[];
  const existing = current.find((item) => item.category === category);
  if (existing?.status === (action === "grant" ? "granted" : "revoked")) return existing;
  const actionPath = action === "grant"
    ? "/v1/auth/consent/grant/{user_id}".replace("{user_id}", encodeURIComponent(userId))
    : "/v1/auth/consent/revoke/{user_id}".replace("{user_id}", encodeURIComponent(userId));
  return request(actionPath, "POST", {
    category,
    purpose: "personalized gameplay analytics",
    transition_id: `${userId}:${category}:${action}`,
  });
}
Enter fullscreen mode Exit fullscreen mode

The read-before-write check is the important line, not the fetch syntax. Replace personalized gameplay analytics with the purpose the player actually sees. A transition ID gives your own consumer jobs a stable deduplication key; store the response and request ID in your audit record.

When the player revokes analytics, stop the event pipeline at its next authorization check. Do not merely flip the toggle green-to-gray. A queued enrichment job must see revoked and discard the payload. Account deletion should also call the session-revocation operation in your identity layer, so a token issued before the decision cannot keep collecting data.

Choosing an implementation without hiding the trade-offs

The surrounding identity system changes how much of this ledger you own. Here is a practical comparison for a small gaming team:

Option Strength Trade-off for consent workflows
Auth0 Mature hosted identity and extensibility Consent events and downstream enforcement still need application code
Okta Customer Identity Strong policy and lifecycle tooling More configuration overhead for a focused preference center
Firebase Authentication Fast mobile integration and broad client support You must design the category-purpose audit model yourself
Infrai One REST API and one credential can cover auth alongside other backend capabilities; the contract stays stable if the vendor behind a capability changes You still own the consent taxonomy, retention policy, and deletion orchestration

Infrai fits when a team values a plain HTTP contract and wants to swap a backend provider without rewriting its application calls. That is a meaningful operational advantage, not a reason to outsource the policy itself.

The catch is scope. A preference center is not suitable when your organization needs a full consent-management suite with jurisdiction-specific banners, legal text versioning, and a large admin console. Stick with a dedicated CMP, or keep Auth0/Okta as the policy system, when those controls are non-negotiable.

Make observability prove the decision was honored

Emit a structured event for every transition: consent.changed, user ID (hashed in analytics), category, old status, new status, purpose version, request ID, and actor. Add a counter for processing attempts blocked by revoked. That counter is often more useful than a dashboard showing how many toggles are on.

The useful operational detail is the join between that event and the work queue. Imagine a player named Mira selecting “personalized gameplay analytics” at 18:02, then revoking it at 18:07 from a phone while a match is still running on a console. The preference service records both transitions. The collector stamps each incoming event with the consent version it observed. A worker that receives an older version compares it with the current view before writing anything. If the versions disagree, it drops the event and increments the blocked counter; it does not try to infer intent from a stale cache. A trace ID carried from the preference request through the queue makes the timeline searchable. This is where the before-and-after model pays off: you can show the exact state that allowed a write, the state that stopped later writes, and the point at which every session was revoked. It also gives support staff a precise answer when a player asks, “Why did this event disappear?”

Small signal. Big payoff.

Alert on impossible sequences, such as data-processing events after a revoke with the same purpose version. Also measure the lag between a revoke and the last accepted event. I am not sure one threshold fits every game; your mileage may vary, so set it from the actual queue and cache behavior you can observe.

For an account deletion request, the runbook should read like a checklist: verify the current consent state, mark the deletion transition, revoke every session, stop category-specific consumers, and retain only the audit evidence your retention policy permits. Each step can be retried and inspected.

Ship with an audit trail and clear boundaries

“Can we just store the latest boolean?” Only if you never need to explain who changed it, which purpose was shown, or what happened during a retry. Regulators and incident responders ask those questions after the fact, when a boolean has already erased the useful context. I've seen teams discover this during a deletion review, when reconstructing one missing event took longer than implementing the endpoint.

“Does revocation require deleting every historical event?” Not automatically. Separate operational data from the minimal audit record required by your policy, then apply the same retention and deletion rules to both. The key behavior is forward-looking: once status is revoked, new processing must stop.

References

Top comments (0)