+

+ The pitch for push approvals is a single sentence: when something + risky is about to happen with your credentials, your phone asks + first. The implementation, of course, is more than that. Here's a + walk through one approval — from the agent reaching for a database + to your thumb committing — and the choices we made about what + crosses the wire and what doesn't. +

+ +

The problem with TOTP

+

+ Six-digit codes from an authenticator app solved one problem + beautifully: they proved you possess a device. They solved nothing + about what you were authorising. A code is a + blank cheque — type it in, and whatever's on the other end of the + login flow proceeds. This is fine when the only thing on the other + end is "log into Gmail". It is not fine when the other end is "let + claude-code write to db-prod-us-east-1 for + two hours." +

+ +
+
+ A code is a blank cheque. We wanted the cheque to show the + amount before you signed it. +
+ — from the original spec, Aug 2025 +
+ +

+ What you actually need to make a confident decision is everything + around the request: who's asking, what they want, + against which resource, for how long, and whether anything about + this looks unusual. Not after you approve. Before. +

+ +

The shape of an approval

+

+ When a request hits the proxy and the matched policy says + approval_mode = per_request, NyxID composes a payload + for your phone. The payload is small on purpose. Here's what + actually shows up: +

+ +
+ +
+ The approval card on iOS — a single screen, four fields, two + buttons. +
+
+ +
    +
  • + Requested by — the agent or service account + making the call. Not a user-supplied label; the platform-bound + identity from the API key. +
  • +
  • + Resource — the downstream service slug and + endpoint, in human terms. postgres://db-prod-us-east-1, + not a UUID. +
  • +
  • + Operation — coarse capability, not the SQL. + READ / WRITE / DEPLOY. +
  • +
  • + Window — how long the grant lives if you say + yes. Always finite. +
  • +
+ +

+ That's it. The push notification itself contains even less — only a + request ID. Details are pulled by the phone over a mutually + authenticated channel when you open the card. If the push payload + leaks (someone screenshotting your lock screen, a misbehaving + backup), there's nothing in it that helps an attacker. +

+ +

Anatomy of the request

+

+ Under the hood, the proxy emits an ApprovalRequest + document, signs it, and pushes it to the device tokens registered + for that user. The agent's HTTP request, meanwhile, is parked on + the proxy's side — held in a per-request state machine with a + countdown. +

+ +
+
+ approval_service.rs + +
+
pub async fn request_approval(
+    db: &Database,
+    actor: &AuthUser,
+    target: &UserService,
+    op: Operation,
+    window: Duration,
+) -> AppResult<ApprovalRequest> {
+    // 1. Compose the minimal payload — what the user must see.
+    let req = ApprovalRequest::new(actor, target, op, window);
+    db.collection::<ApprovalRequest>("approval_requests")
+      .insert_one(&req).await?;
+
+    // 2. Push only the request_id. Details are fetched on tap.
+    push_service::notify(actor.user_id, &req.id).await?;
+
+    // 3. Audit before we wait.
+    audit::log("approval.requested", &req).await?;
+    Ok(req)
+}
+
+ The function is short on purpose. Anything that can fail before + the push is sent gets logged — anything that comes after lives in + the audit trail. +
+
+ +

+ Three things happen in order: the request lands in MongoDB, a push + with only the request ID goes out, and the audit log gets a row. + If any step fails the request never reaches your phone, and the + agent gets a clean error code (1010 — approval pending + transitions to 1012 — approval failed). +

+ +
+ We deliberately don't send the operation parameters in the push + envelope. The operation category is enough for the lock + screen; the operation contents are pulled on tap, behind + biometric auth. +
+ +

What the tap actually does

+

+ Tapping Approve mints a short-lived grant — a + signed token bound to this request, this resource, + and the window you saw on screen. The proxy's parked request is + woken up, the grant is checked, and the call continues to the + downstream service. Tapping Deny writes a denial + row and returns 1011 — approval denied to the agent. + Either way, the round-trip is logged with attribution down to the + API key. +

+ +

What we deliberately don't send

+

+ A surprising amount of the design lives in the things we kept out + of the wire. The shortlist: +

+ +
    +
  1. + The actual API call body. The proxy holds it; your phone never + sees it. +
  2. +
  3. + Any credentials at all. Tokens, keys, secrets — none of these + ever reach the device. The mobile app holds session JWTs in the + keychain, nothing else. +
  4. +
  5. + Free-form labels supplied by the requesting agent. The agent + identity is platform-bound and signed by the issuance flow. +
  6. +
  7. + Cross-tenant context. If your user belongs to two orgs, the + approval is scoped to the org that owns the resource — not + merged. +
  8. +
+ +

+ The thing we're protecting is the integrity of the decision + — that what you see is what gets approved, and what you see is + enough to decide. Everything outside that goal is overhead. +

+ +

Try it

+

+ If you're in the beta, nyxid service add --slug + postgres-prod --approval per_request turns this on for any + service you've registered. The mobile app reaches you within a + second on most carriers; if your network is slow, the proxy will + wait a configurable window (default 30s) before failing closed. + Curious about the rest? + Here's the credential broker walkthrough + — the piece of the system that makes "your applications never see + the keys" technically true. +

+ + + + + +
+
PR
+
+
Written by
+

Priya Ramesh

+

+ Product at NyxID. Previously: identity at a healthtech you + haven't heard of, and an embedded systems lab where she got + very tired of TOTP. Writes about authorisation, mobile flows, + and the surprisingly philosophical question of "what counts as + consent." +

+ +
+
+ + +
+ + +
+