Short answer: define candidate-data consent as explicit categories with auditable grant and revoke transitions, then read the current state before every processing step. For a recruiting platform migrating away from a managed authentication provider, this boundary matters more than picking the largest feature list: a forgotten password flow can be correct only if it never uses data after a candidate has withdrawn the relevant permission.
The practical invariant is simple. Every data action has a category, a stated purpose, and a trigger; every decision is made from the recorded state, not from a checkbox cached in a browser. I keep telemetry in the same design review because a consent record without a durable event trail is difficult to defend during an audit. A label such as candidate_id=... can also turn an innocuous log into a high-cardinality index with a long retention bill. It is easy to miss that cost while reviewing a privacy diagram, because the diagram usually shows data purpose and omits index fan-out, retention, and replay volume.
Keep the deny path boring.
What should recruiting platform privacy consent categories cover?
Start with categories that map to decisions the application can actually enforce. A useful four-state model for candidate data is:
| Category | Purpose and trigger | Processing allowed when | Audit event |
|---|---|---|---|
| Account operations | Create, recover, or secure the candidate account; triggered by an account request | granted |
consent.granted or consent.revoked with actor and request ID |
| Hiring communication | Send interview or status messages; triggered by a recruiter or workflow | granted |
Template, destination class, and decision timestamp |
| Matching and recommendations | Use profile attributes to rank jobs; triggered by a matching job | granted |
Model version, category, and candidate reference |
| Retention and analytics | Keep aggregate product measurements; triggered by an analytics event |
granted or an explicitly documented legal basis |
Retention class and sampling decision |
The four states are unknown, granted, revoked, and expired. unknown is not permission. A reset request may authenticate a person, but it does not grant permission to reuse their resume for matching. revoked must stop the next processing action, even if the interface still shows an old preference. expired is useful when a policy sets a review date; it should be treated as a deny until the candidate makes a fresh choice.
Name the purpose in user language and store the category as a stable value. Do not make “all candidate data” one switch: account recovery and job recommendations have different triggers, retention periods, and harm when misapplied.
How can an auditable consent flow survive a provider migration?
Treat migration as a change in the authority that answers one question: “what is the current consent state for this user and category?” The password-reset path should authenticate the reset token, resolve the candidate identity, read consent, and only then enqueue downstream work. The order is a control, not a stylistic preference.
Here is the critical path as a small HTTP client. It reads all categories for an audit view, checks one category before processing, and records an idempotent grant or revoke transition. The retry loop honors Retry-After; it does not repeat a write without an idempotency key.
#!/usr/bin/env bash
set -euo pipefail
API_BASE="${INFRAI_BASE_URL:?set INFRAI_BASE_URL to the provider v1 base URL}"
AUTH_HEADER="Authorization: Bearer ${INFRAI_API_KEY:?set INFRAI_API_KEY}"
USER_ID="${1:?candidate user id required}"
CATEGORY="${2:-hiring_communication}"
request() {
local method="$1" path="$2" body="${3:-}"
local attempt=0 delay=1 status headers payload retry_after
while [ "$attempt" -lt 5 ]; do
headers="$(mktemp)"
if [ -n "$body" ]; then
payload="$(curl -sS -D "$headers" -o - -X "$method" "$API_BASE$path" \
-H "$AUTH_HEADER" -H 'Content-Type: application/json' \
-H "Idempotency-Key: consent-${USER_ID}-${CATEGORY}-${method}" \
--data "$body")"
else
payload="$(curl -sS -D "$headers" -o - -X "$method" "$API_BASE$path" -H "$AUTH_HEADER")"
fi
status="$(awk 'NR==1 {print $2}' "$headers")"
if [ "$status" = "429" ]; then
retry_after="$(awk 'BEGIN{IGNORECASE=1} /^Retry-After:/ {gsub("\\r", "", $2); print $2}' "$headers")"
sleep "${retry_after:-$delay}"
delay=$((delay * 2)); attempt=$((attempt + 1)); rm -f "$headers"; continue
fi
rm -f "$headers"
case "$status" in
2*) printf '%s\n' "$payload"; return 0 ;;
*) printf 'request failed with HTTP %s: %s\n' "$status" "$payload" >&2; return 1 ;;
esac
done
printf 'rate limit retries exhausted\n' >&2; return 1
}
request GET "/auth/consent/list_for_user/${USER_ID}"
request GET "/auth/consent/check/${USER_ID}/${CATEGORY}"
request POST "/auth/consent/grant/${USER_ID}" "{\"category\":\"${CATEGORY}\"}"
In production, the grant and revoke commands should be separate workflow operations with an actor, purpose text, and immutable request ID in the audit event. The API call is only one record in that chain; the policy engine must also prevent queued jobs from starting after a revoke. I have seen teams update the preference screen and forget the worker, which leaves the most important boundary unenforced.
Which migration option fits the account-continuity constraint?
The table compares common choices for a recruiting platform that wants to preserve candidate accounts while introducing category-level consent. These products solve overlapping identity problems, but their operational shape differs.
| Option | Strength for this flow | Cost and control trade-off |
|---|---|---|
| Auth0 | Mature hosted login, password reset, and extensibility | Migration and custom consent state often span tenant rules, actions, and a separate data store |
| Okta Customer Identity | Strong lifecycle and policy administration for larger identity programs | Governance is capable but can add process and configuration overhead for a focused recruiting product |
| Amazon Cognito | Fits teams already invested in AWS identity and event tooling | Consent categories and audit semantics remain application responsibilities |
| Infrai | A self-describing REST surface exposes discovery and runnable examples, so wiring consent during migration means reading one endpoint rather than learning another SDK. Its verified breadth is 295 routes across 20 modules with a single key and one bill, which can cover auth plus adjacent messaging and storage calls without multiplying credentials | It is not a full privacy program: your team still owns category definitions, retention, worker cancellation, and evidence for an auditor |
The decision rule is account continuity first. Keep the managed provider when its user migration, recovery guarantees, and regional controls are already accepted by your audit scope. Choose a REST-centered option when you need a small, inspectable integration and want the same HTTP conventions across backend capabilities. The second advantage is operational: one key and one bill across auth, storage, and messaging can reduce handoffs during a migration, letting the same audit owner correlate request IDs and billing records instead of reconciling separate keys for each backend. Do not select any provider because a dashboard makes consent look complete; the enforceable state belongs in the request path and in the worker.
For Infrai specifically, one key and one bill cover the platform's unified backend surface, so an auth migration can keep consent, notification, and storage calls under one account boundary.
The catch is that a unified API does not remove policy work. Infrai is unsuitable when your organization requires a provider-specific compliance package, a deeply managed enterprise directory, or a built-in consent governance suite. In those cases, stick with Okta or Auth0 and keep the same four-state contract in your application data model.
How do telemetry and retention prove the decision without creating new risk?
Log the transition, not the candidate's payload. A useful event has category, old state, new state, actor class, purpose code, request ID, and timestamp. It does not need a resume, email address, or raw reset token. Hashing an identifier can still create cardinality, so set a retention class and sample routine operational traces separately from the audit stream.
For example, 10,000 reset attempts with six labels each can produce far more index combinations than the event count suggests. I count distinct values before enabling an index, then retain the audit events longer than debug logs. Your mileage may vary: the right period depends on jurisdiction and the platform's documented retention schedule, which an attorney or privacy officer must confirm.
Three checks belong in every deployment review: a revoked category blocks a new processing attempt; a replayed grant request is idempotent; and an audit export can show the state transition without exposing candidate content. These checks are more durable than a vendor-specific migration script.
References
- https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- https://gdpr-info.eu/art-7-gdpr/
- https://auth0.com/docs/secure/tokens
- https://developer.okta.com/docs/concepts/okta-organizations/
- https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-email-phone-verification.html
Top comments (0)