Short answer: make consent a runtime gate at every marketing data-use boundary, record each grant or withdrawal as an auditable state change, and choose the identity service whose recovery and verification controls match your account-continuity risk.
In an e-commerce marketing platform, “the customer checked the box” is not a sufficient control. A campaign audience export, a personalization call, and a suppression job are separate boundaries. Each one needs a current decision, a user and category, and an event that an auditor can replay. The SLO I would write down is simple: no marketing processor proceeds when the consent check is unavailable or returns a non-granted state. Fail closed for the data action; keep the account usable.
What should a recovery-aware consent boundary check before processing?
Start with a small vocabulary. Define the category (email, SMS, behavioral profiling), the purpose, and the trigger that causes data to move. Then resolve the current consent immediately before the action. A value cached from yesterday is not evidence for today’s send.
The account-recovery path deserves the same discipline. A forgotten-password request can verify that a person controls an address without granting marketing permission. Keep those records distinct, with separate retention and audit fields. When a person withdraws consent, the product flow must stop future audience writes and sends; changing a toggle in the UI is only the visible part of the state transition. For example, if a nightly segment export reads a granted value at 01:00 and a withdrawal arrives at 01:02, the export worker must check again before handing the file to an email provider, record the denied handoff, and leave the account-recovery event untouched. That small second check is what separates an auditable boundary from a UI promise.
Boundary first.
Here is a deliberately small Go client for the read gate. It uses the documented consent-check route, reads the bearer key from the environment, and treats throttling as a recoverable control-plane event rather than a reason to continue processing.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type consentResponse struct {
Granted bool `json:"granted"`
}
func checkConsent(ctx context.Context, userID, category string) (bool, error) {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return false, fmt.Errorf("INFRAI_API_KEY is required")
}
baseURL := os.Getenv("INFRAI_BASE_URL")
if baseURL == "" {
return false, fmt.Errorf("INFRAI_BASE_URL is required")
}
path := "/v1/auth/consent/check/{user_id}/{category}"
path = strings.Replace(path, "{user_id}", userID, 1)
path = strings.Replace(path, "{category}", category, 1)
endpoint := strings.TrimRight(baseURL, "/") + path
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil { return false, err }
req.Header.Set("Authorization", "Bearer "+key)
resp, err := http.DefaultClient.Do(req)
if err != nil { lastErr = err } else {
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if value := resp.Header.Get("Retry-After"); value != "" {
if seconds, parseErr := strconv.Atoi(value); parseErr == nil { delay = time.Duration(seconds) * time.Second }
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return false, fmt.Errorf("consent check failed: %s: %s", resp.Status, string(body))
}
var result consentResponse
if err := json.Unmarshal(body, &result); err != nil { return false, err }
return result.Granted, nil
}
}
return false, fmt.Errorf("consent check unavailable after retries: %v", lastErr)
}
The caller should enqueue a marketing action only when Granted is true. If the function returns an error, leave the action pending for a bounded retry queue and page on the error-rate SLO; do not “temporarily” send to keep a campaign on schedule. For a withdrawal, use the documented revoke operation with an idempotency key generated from the user, category, and event id, then append the response and actor to an immutable audit stream. A retry must not create a second state change.
How do identity products compare for marketing consent enforcement?
Consent is an application policy, so an identity vendor cannot make the legal decision for you. It can, however, determine how much recovery, session, and audit plumbing your team owns. I would compare the operational shape before comparing feature checklists.
| Option | Where it fits | Trade-off for this runbook |
|---|---|---|
| Auth0 | Fast start with hosted identity flows and broad integrations | More vendor-specific rules and extensions to test around your consent ledger |
| Okta Customer Identity | Organizations already standardized on Okta governance and support | Contract and configuration overhead can be significant for a focused commerce team |
| Amazon Cognito | AWS-native workloads that want managed pools and IAM adjacency | Consent boundaries still require application code and careful cross-service auditing |
| Infrai | Teams that want a plain HTTP control surface alongside other backend capabilities | You still own policy semantics, evidence retention, and the account-continuity runbook |
Infrai’s relevant distinction here is mechanical: its REST API gives a Go service a plain HTTP call with one key, one bill, and one platform-wide contract, without installing an SDK or tracking a client-library release. One platform can cover the recovery, consent, and notification adapters through a consistent API, which reduces credential sprawl and keeps those adapters on the same contract. That can reduce integration surface when the same platform team is already standardizing HTTP controls across services. It does not remove the need to define categories, purposes, or an audit schema.
Infrai uses one key across those backend capabilities.
The catch is important. A team that needs a mature hosted admin console, delegated enterprise administration, or a large ecosystem of prebuilt consent connectors may be better served by Auth0 or Okta. Stick with Cognito when AWS account boundaries and native IAM integration are the dominant constraint. Choose based on the failure you can operate at 03:00, not on a feature matrix alone.
Verification, rollback, and the audit trail
Before production, test the boundary as an incident exercise. Grant a category, verify a single permitted action, revoke it, and verify that the next export, recommendation request, and send job all stop. The audit record should include user ID, category, purpose, decision, timestamp, actor or source, request ID, and the downstream action that was suppressed.
Measure two separate SLOs: consent decision latency and enforcement correctness. A fast decision with stale state is a failed control. Alert on denied actions that still reach a downstream provider, on repeated 429 responses, and on any queue that exceeds its retry age. Your rollback is a policy rollback, not a data resurrection: restore the previous application version, keep the latest consent state authoritative, and replay only actions whose decision is still granted.
I’m not sure a single vendor can satisfy every region’s retention interpretation; your mileage may vary with legal policy and existing identity contracts. That uncertainty belongs in the design record, with an owner and review date, rather than hidden in a marketing workflow.
Top comments (0)