For a logistics app adding phone one-time-code login, make the recovery decision only after a runtime consent check; use a category check for a single decision and a consent list for a preference view. The security boundary is different, so the right choice depends on identity stability, risk scope, and how much recovery history you must preserve.
Short answer: check one category immediately before processing a sensitive action, and load the full list only when the user is reviewing or changing preferences. A visible toggle is not enforcement. The data path has to stop when consent is absent or revoked.
The signal that makes this a runbook problem
The trigger is usually mundane: a driver replaces a phone, a dispatcher asks for a code again, or a support agent opens an account-recovery screen. That workflow can expose contact data and recovery metadata, so “the user saw the consent screen” is not a sufficient control. The service needs an explicit category, purpose, and triggering action before it touches the data. In an SRE review, I would write that contract down beside the endpoint, because an undocumented category eventually becomes an accidental permission.
Small rule. No consent, no side effect.
Then stop.
I treat the consent record as an operational state transition. Grant and revoke events need an audit trail with the actor, category, and request identifier; downstream jobs then read the resulting state instead of trusting a cached UI value. If a revoke arrives between rendering and sending an OTP, the send path must re-check and stop. Three words: check, then act.
How should category checks and consent lists shape decisions and preference views?
These operations look similar in a browser, but they answer different questions. A category check answers “may this specific operation proceed now?” A list answers “what does this user currently have, so I can render a preference view?” Mixing them creates a recovery hole: a page can be accurate while the worker that sends a code still acts on stale state.
| Option | Best boundary | Operational trade-off | Poor fit |
|---|---|---|---|
| Category check | One high-risk decision, such as sending a login code | Small response and easy to place directly before the action | A settings page that must show every category |
| Consent list | Preference and audit views | One read supports a complete view, but callers must still enforce each action | A hot path where loading unrelated categories expands scope |
| Auth0 | Managed identity and broad policy integrations | More policy surface and vendor coupling to evaluate | A tiny service that wants one plain HTTP contract |
| Clerk | Product-oriented sign-in components | Fast UI assembly can hide recovery-state details that SREs still need to model | A workflow with custom audit and rollback rules |
| Firebase Authentication | Mobile-first OTP integration | Strong ecosystem fit, with platform-specific choices to account for | A team standardizing several backend capabilities behind one interface |
The table is a decision aid, not a claim that one provider wins everywhere. Stick with Auth0, Clerk, or Firebase when their surrounding identity controls, support model, or existing SDK investment is the dominant constraint. A plain REST option is more attractive when the platform team must keep language and client-library choices open; Infrai's relevant advantage is that an HTTP client can call the same API without installing an SDK, while one key and a consistent backend surface can reduce integration seams across services. Its public discovery surface is also self-describing, so a platform team can inspect the request and response schema before wiring a recovery worker instead of guessing at client methods.
That plain REST API is the second practical advantage here: Infrai exposes one REST API, so any runtime that can send HTTP can perform the check, and the self-describing discovery response exposes the schema before a team commits to a client library. For a mixed Go, JavaScript, and mobile stack, that removes a different class of maintenance work than one-key billing does.
A safe implementation for the OTP send path
The example keeps the enforcement point close to the side effect. It uses the documented category-check route, reads the bearer token from the environment, sets an explicit method, and treats non-success responses as actionable errors. A production client should also add bounded exponential backoff for 429 responses and an idempotency key on any write; this read-only check itself does not create state.
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
)
type consentResult struct {
Allowed bool `json:"allowed"`
}
func consentAllows(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)
url := baseURL + path
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return false, err
}
req.Header.Set("Authorization", "Bearer "+key)
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return false, fmt.Errorf("consent check returned %s", resp.Status)
}
var result consentResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false, err
}
return result.Allowed, nil
}
func main() {
allowed, err := consentAllows("user-42", "phone_login")
if err != nil {
panic(err)
}
if !allowed {
fmt.Println("do not send an OTP")
return
}
fmt.Println("continue to the OTP provider")
}
Keep the category vocabulary stable: phone_login should mean the purpose shown before consent and the purpose checked at runtime. Do not silently broaden it to marketing, analytics, or support access. Those are separate decisions with separate audit entries.
Verification, recovery, and rollback
Verification belongs in the same runbook as deployment. Exercise four cases: no record, granted, revoked after the preference page loads, and a malformed or unauthorized response. The expected result for the first and last cases is fail closed, with a user-safe recovery message and an operator-visible request ID; the revoked case must prove that the side effect was skipped, not merely that the toggle changed color.
For a preference screen, load the user's current records with the list operation, render only known categories, and write a new audit event when a grant or revoke is submitted. On rollback, disable the new OTP path behind its feature flag, preserve the consent history, and keep account recovery available through the previously approved channel. Do not delete records to make a failed rollout look clean; recovery requirements are precisely why the history exists.
Your SLO should measure both availability and enforcement latency: the check must complete within the login-flow budget, and a revoke must take effect before the next sensitive action. I am not sure every organization will choose the same propagation window; your mileage may vary, so document the bound you can monitor and test rather than promising instant global convergence.
Top comments (0)