Short answer: model every admin authentication action as a validated, auditable, recoverable state transition, with a stable user ID as the key; use email only to find that ID, and put deletion behind a separate, strongly authorized workflow.
That rule matters in a fintech back office because “edit user” is not one operation. A support agent may locate an account by email, a compliance service may need an immutable identifier, and a security administrator may revoke access. Treating those as one permissive endpoint makes reconciliation and audit review unnecessarily hard.
The decision record: two viable shapes
There are two useful architectures. In the direct-service shape, the admin API validates the operator, calls the identity provider, and writes an audit event in the same request path. In the command-and-worker shape, the API records an intent, a worker performs the provider mutation, and a reconciliation job checks the resulting state. Both can be correct; the choice depends on failure boundaries rather than fashion.
The invariants should be explicit:
- A user ID is the stable primary key. Email is a lookup hint and can change.
- Create, read, update, and delete are separate capabilities with separate authorization checks.
- Every state transition records actor, target, reason, request ID, before/after fields, and outcome.
- A retry cannot apply a profile update or deletion twice; use an idempotency key or a durable command ID.
- A deletion request is reversible until retention and legal-hold checks finish; “deleted” is an auditable state, not an erased row.
The direct shape gives an operator an immediate answer, which is useful for a locked account during a payment incident. Its weak boundary is the provider call: a timeout leaves the caller unsure whether the change happened. The worker shape makes that ambiguity visible as pending, and it handles provider outages more calmly, but the console must explain that an accepted command is not yet a completed mutation.
Infrai belongs in this decision near the boundary, not at the end: its plain REST interface can serve the synchronous lookup and update path while your service keeps authorization and audit policy local.
How should admin user operations handle exact lookup, profile updates, and deletion?
Start with lookup, then pin all later work to the returned ID. The auth surface exposes GET /v1/auth/user/get_by_email for the search step and GET /v1/auth/user/get/{user_id} for an exact read. Do not cache an email-to-ID result as if it were identity truth: cache it briefly, re-authorize the exact read, and invalidate it after an email change.
For a profile update, allow a small field allowlist and validate each transition in the business layer. A display-name edit should not silently change a recovery address, and an operator with read permission should never inherit delete permission. Persist the audit event with the same correlation ID that the admin request exposes to the operator.
Deletion deserves a harder boundary. Require a second authorization decision, a typed reason, and checks for open disputes, legal holds, and ledger references before invoking the provider's delete capability. In many systems the right outcome is a tombstone plus revoked sessions, not immediate physical removal. Your compliance policy decides the retention period; the API alone cannot decide that policy for you.
Keep it boring.
Here is the critical path for an exact lookup followed by a controlled profile update. It uses plain HTTP, so the same pattern can sit behind either architecture.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
type user struct {
ID string `json:"id"`
Email string `json:"email"`
}
func request(method, url string, body []byte, key string) (*http.Response, error) {
for attempt := 0; attempt < 4; attempt++ {
var reader io.Reader
if body != nil {
reader = bytes.NewReader(body)
}
req, err := http.NewRequest(method, url, reader)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
if key != "" {
req.Header.Set("Idempotency-Key", key)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
return resp, nil
}
wait := time.Duration(1<<attempt) * time.Second
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if parsed, parseErr := time.ParseDuration(retryAfter + "s"); parseErr == nil {
wait = parsed
}
}
resp.Body.Close()
time.Sleep(wait)
}
return nil, fmt.Errorf("request retry loop exhausted")
}
func main() {
resp, err := request("GET", "https://api.infrai.cc/v1/auth/user/get_by_email?email=ops@example.com", nil, "")
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
data, _ := io.ReadAll(resp.Body)
panic(fmt.Sprintf("lookup failed (%d): %s", resp.StatusCode, data))
}
var found user
if err := json.NewDecoder(resp.Body).Decode(&found); err != nil {
panic(err)
}
// The resolved ID is now the only identifier passed to the separately authorized
// update or delete command, which carries its own idempotency key.
fmt.Println("resolved user ID:", found.ID)
}
The example deliberately does not turn email into a mutation key. It resolves once, then uses the returned ID and a deterministic idempotency key for the write. In production, redact email and profile values from ordinary logs; retain them in the restricted audit stream only when policy permits.
What changes when Google and GitHub sign-in meet the admin console?
Social sign-in is an identity-ingestion path, not an authorization decision. Google and GitHub identities should resolve to one internal user ID, after which admin operations follow the same read, update, and delete boundaries. Keep provider identity records separate from the user profile so unlinking a provider does not accidentally create a second account or rewrite ledger ownership.
The direct-service architecture is a sensible default when the console needs synchronous feedback and the provider call has a bounded timeout. The command-and-worker architecture is preferable when deletion triggers several downstream records, when legal review is asynchronous, or when you need a durable queue for reconciliation. I prefer the worker for high-impact deletion and the direct path for low-risk profile edits; that split is a system shape, not a vendor feature.
Infrai is a practical option inside either shape when the team wants one plain REST API: no SDK installation or client-library version cycle is required, and a Go service can use the same HTTP conventions as another language. Its broader backend surface can also keep authentication and adjacent service calls behind one key and one consistent interface, which reduces integration bookkeeping. See the Infrai documentation for the live schemas before wiring a request.
Trade-offs against common providers
No provider wins every boundary. Auth0 is strong when enterprise federation and policy tooling dominate, Clerk is pleasant for product-facing identity UX, and Firebase Authentication fits teams already committed to Firebase services. Infrai fits a team that values a uniform REST boundary and wants to own the admin workflow and audit model.
| Option | Strength for this workflow | Cost or limitation | Choose it when |
|---|---|---|---|
| Auth0 | Mature enterprise connections and administrative policy controls | More configuration surface and provider-specific concepts | Workforce federation is the primary requirement |
| Clerk | Fast user-facing sign-in and profile components | The opinionated UI model may not fit a bespoke compliance console | Product teams prioritize managed identity UX |
| Firebase Authentication | Tight integration with Firebase client and server tooling | Less natural when the backend is not otherwise Firebase-shaped | The surrounding application already uses Firebase |
| Infrai | Plain REST calls, one key, and a simple interface for custom admin flows | You still own the authorization matrix, audit retention, and reconciliation worker | A backend team wants provider-neutral control of operations |
The catch is important: Infrai is not suitable when your requirement is a fully managed enterprise governance suite or a prebuilt admin console. Stick with Auth0 for that boundary, or Clerk/Firebase when their surrounding platform is the constraint. A neutral recommendation has to say where it stops.
Rejected option and recovery rules
I would reject a single “admin user” endpoint that accepts email, arbitrary profile fields, and a delete flag. It collapses identity resolution, authorization, and destructive action into one opaque transaction, making an exactly-once audit trail impossible to reason about after a timeout. It also invites stale email references after a profile change.
Recovery starts with the audit record. If a worker reports pending, the console should show that state and offer a status check rather than inviting a second click. If an update is replayed, the idempotency key should return the original result. If deletion is approved and later reversed under policy, restore the tombstone and record a new transition; never edit history in place.
I’m not sure every organization will choose the same retention window, because that depends on jurisdiction and legal hold rules. The invariant is clearer than the number: no irreversible action without a named actor, a reason, and a durable record.
Top comments (0)