Open in Busymate DevTools (one-tap)

The partner integration guide for the one-tap 'Open in Busymate DevTools' hand-off — drop your users into Busymate DevTools, signed in as themselves, in ONE tap. The confirmed same-device PKCE contract (mint verifier + S256 challenge, broker /start, open bmdev://auth/federation?code=…#fed_verifier=…, /exchange for a real-email session), how to onboard a partner with one upsert_federation_client admin action, and the BMFederationExample reference implementation.

Open in Busymate DevTools — the one-tap partner hand-off

Drop your users into Busymate DevTools, signed in as themselves, in ONE tap. Any partner project — BM Pro, the BusyDrivers portal, a future partner — adds a single "Open in Busymate DevTools" button. A user who is logged into your app taps it once and lands in Busymate DevTools already signed in with their real account. No device pairing, no unpair, no logout, no reinstall, no second login screen.

This is the partner-facing integration guide — the copy-paste recipe a partner follows. For anything this page doesn't answer (the exhaustive endpoint/error reference, the account-provisioning policy, the security model), ask the Busymate team.

The whole integration is small and generic — there is no per-partner code on Busymate's side. The broker is one function driven entirely by your federation_clients row, so onboarding a new partner is a single admin action (see Onboard a partner).

The user experience

One button, two states — that's the entire UX contract:

  • Logged in to your app → the button reads "Open in Busymate DevTools" → one tap → the user is in Busymate DevTools, signed in as themselves.
  • Not logged in → your app shows its normal login → the user signs in → the same button appears → one tap → in.

That's it. The user never sees a Busymate login screen, never copies a token, never re-types credentials. If they were already signed into a different Busymate account, this switches the session in place to the partner user — no manual sign-out.

The hand-off in one diagram

partner appPOST /startopen the app (deep link)POST /exchangeBusymateDevToolsyour backend sessioncode_challenge = S256(verifier)?code=… #fed_verifier=…code + code_verifierreal session, real email

The single load-bearing idea: the code travels in the URL query (server-visible), but the PKCE verifier travels in the URL fragment (#…), which is client-only — never transmitted to a server, never written to an access log or Referer. So a leaked link is useless: the code alone can't be redeemed without the verifier, and the verifier never leaves the device.

The confirmed contract — four steps

This is the exact wire contract (one-tap, PKCE), verified end-to-end against the live broker, the Busymate DevTools app's deep-link handler, and the BMFederationExample reference partner app.

Step 1 — mint a PKCE pair

The partner generates a PKCE pair on the device (RFC 7636, S256):

  • code_verifier — a high-entropy random string, base64url encoded (the reference uses 32 random bytes → 43 chars, no padding, URL-safe -/_ alphabet).
  • code_challenge = BASE64URL( SHA256( code_verifier ) ), method "S256". Hash the verifier's raw ASCII bytes, not any decoded value.

Keep the code_verifier in memory on the device — you'll pass it to Busymate DevTools at the end, on-device only. Send only its challenge to the broker.

Step 2 — call the broker /start

Vouch your user to the broker with the challenge and a credential. How the broker verifies who you're vouching depends on your client's verify_mode (set once at onboarding) — and the matching credential field is required: a credential-less /start is rejected (you can only hand off a user your backend actually has signed in).

directus_bearer (the BM Pro / BusyDrivers shape) — add the user's current backend access token as "directus_token". The broker reads the identity from a live GET {your_base}/users/me call — never from your body, so it can't be spoofed:

http
POST https://api.busymate.net/functions/v1/federation-broker/start
content-type: application/json
 
{
  "client_id": "bm_yourapp",
  "code_challenge": "<BASE64URL( SHA256(code_verifier) )>",
  "code_challenge_method": "S256",
  "target": "ios",
  "directus_token": "<the user's current backend access token>"
}

Passing the token as an Authorization: Bearer <token> header works too — the body directus_token takes precedence if both are present.

hmac — for a partner with no introspection endpoint: sign a small canonical payload with your Busymate-issued client secret (signature = BASE64URL( HMAC-SHA256(secret, canonical(payload)) ), canonical = sorted-key JSON with undefined dropped, iat ∈ [now-120s, now+30s], single-use jti). Send the payload + signature in the body:

http
POST https://api.busymate.net/functions/v1/federation-broker/start
content-type: application/json
 
{
  "client_id": "bm_yourapp",
  "code_challenge": "<BASE64URL( SHA256(code_verifier) )>",
  "code_challenge_method": "S256",
  "target": "ios",
  "payload":   { "sub": "ext-42", "email": "user@yourapp.example", "iat": 1750000000, "jti": "uniq-1" },
  "signature": "<BASE64URL HMAC-SHA256 over canonical(payload)>"
}

supabase — for a partner whose product runs on its own Supabase project: send the user's live Supabase access token as "supabase_token" (verified against your project's public JWKS — ES256/RS256 signing keys required). The full recipe, prerequisite, and troubleshooting live in Federate a Supabase userbase.

The response carries a single-use, 60-second, PKCE-bound code on a target-aware link:

jsonc
{ "link": "https://busymate.net/app/auth/federation?code=<C>", "expires_in": 60, "target": "ios" }

You only need the ?code=<C> value from link. (target may be "ios" for the app hand-off or "universal" for the dual app/web link; both carry the same ?code=.)

Step 3 — open Busymate DevTools on the SAME device

Open the app with the code in the query and the PKCE verifier in the URL fragment:

bmdev://auth/federation?code=<C>#fed_verifier=<V>
  • bmdev is Busymate DevTools' registered custom URL scheme — this opens the app directly, no Universal-Link / AASA association required. (busymate-devtools://… is an accepted alias.)
  • code=<C> is the broker code from Step 2 (the query — server-visible, but useless alone).
  • #fed_verifier=<V> is your Step-1 code_verifier (the fragment — client-only, never sent to a server). The fragment key is exactly fed_verifier.

Universal Link equivalent (same route, AASA-claimed on busymate.net + dash.busymate.net):

https://busymate.net/app/auth/federation?code=<C>#fed_verifier=<V>

The custom scheme is the simplest, most reliable transport — it opens the app even on a build that never associated the AASA. Open it promptly: the code's TTL is 60 seconds.

Step 4 — Busymate DevTools redeems and signs in

Busymate DevTools reads the code (query) and the verifier (fed_verifier fragment) and POSTs:

http
POST https://api.busymate.net/functions/v1/federation-broker/exchange
content-type: application/json
 
{ "code": "<C>", "code_verifier": "<V>" }

The broker runs verifyPkceS256 — it re-derives BASE64URL(SHA256(code_verifier)) and matches it to the code_challenge you sent at /start. On a match it mints a real Supabase (GoTrue) session for the partner user with their real email, and Busymate DevTools signs in — switching the session in place from any account that was active. No pairing, no unpair, no owner-bind dance, no logout, no second login screen.

The /exchange response is the real session triple plus identity:

jsonc
{
  "access_token":  "<real Supabase access token>",
  "refresh_token": "<real Supabase refresh token>",
  "expires_in":    3600,
  "user_id":       "<auth.users id>",
  "email":         "<the partner user's real email>",
  "login_source":  "bm_yourapp"
}

You (the partner) never touch a Busymate token. Your app only ever opens the hand-off URL — Busymate DevTools is the side that calls /exchange and holds the session. The single-use code in the link is the only thing that crosses, and it's unredeemable without the matching verifier.

Integration contract (any platform)

The four steps above are language- and platform-neutral. iOS is just the reference; a web partner, an Android app, or any other client reproduces the same four HTTP / deep-link steps. This section is the bare contract.

The four steps as plain HTTP

  1. Mint PKCE (on the device/client): code_verifier = base64url(random 32 bytes); code_challenge = base64url( SHA256( code_verifier ) ), method "S256". Keep the verifier client-side.
  2. POST /start with the challenge and your credential → a single-use, 60-second, PKCE-bound ?code=.
  3. Open the hand-off on the same device: bmdev://auth/federation?code=<C>#fed_verifier=<V> — the code in the query, the verifier in the client-only fragment.
  4. Busymate DevTools POST /exchange { code, code_verifier } → a real Supabase session for the partner user, with their real email. (You never call /exchange — Busymate DevTools does.)
bmdev://auth/federation?code=<C>#fed_verifier=<V>
  • bmdev is Busymate DevTools' registered custom URL scheme — it opens the app directly, no AASA / Universal-Link association required. (busymate-devtools://… is an accepted alias.)
  • code=<C> — the broker code from /start (the query; server-visible, useless alone).
  • #fed_verifier=<V> — your Step-1 code_verifier (the fragment; client-only, never sent to a server). The fragment key is exactly fed_verifier. URL-encode both values.
  • The AASA equivalent https://busymate.net/app/auth/federation?code=<C>#fed_verifier=<V> resolves to the same route on a build that associated the domain.

POST /start — request / response

jsonc
// request — directus_bearer (the body carries the credential; the broker re-verifies it live):
{
  "client_id": "bm_yourapp",
  "code_challenge": "<base64url(SHA-256(code_verifier))>",
  "code_challenge_method": "S256",
  "target": "ios",                                  // open the native app via bmdev://
  "directus_token": "<the user's backend access token>"
}
// — OR hmac: replace directus_token with
//   "payload":   { "sub": "...", "email": "...", "iat": <sec>, "jti": "<single-use>" },
//   "signature": "<base64url HMAC-SHA256 over canonical(payload)>"
 
// response (200) — the link carries the CODE only, never a token:
{ "link": "https://busymate.net/app/auth/federation?code=<C>", "expires_in": 60, "target": "ios" }
// You only need the ?code=<C> value from `link`.

POST /exchange — request / response (called BY Busymate DevTools)

jsonc
// request — the code + the matching PKCE verifier (the possession proof):
{ "code": "<C>", "code_verifier": "<V>" }
 
// response (200) — the real Supabase session, minted by user_id, with the real email:
{
  "access_token":  "<real Supabase access token>",
  "refresh_token": "<real Supabase refresh token>",
  "expires_in":    3600,
  "user_id":       "<auth.users id>",
  "email":         "<the partner user's real email>",
  "login_source":  "bm_yourapp"
}

Errors

StatusEndpointMeaning
400/startBad/missing client_id, code_challenge, code_challenge_methodS256, or a missing credential. /exchange: missing code or code_verifier.
401/startUnknown / disabled client (opaque), or the credential was rejected (Directus bearer invalid, HMAC bad-signature/expired/replayed jti). /exchange: PKCE verification failed (the verifier doesn't match the stored challenge).
410/exchangeThe code is Gone — already used (single-use, consumed on first exchange), expired (> 60 s), or unknown. The three are indistinguishable on the wire.
429bothRate limited (per-client + per-IP; fail-closed).

What the partner supplies vs what Busymate does

StepThe partner (your side)Busymate (the broker + the app)
1 · PKCEMint code_verifier + code_challenge on the device; keep the verifier client-side.
2 · /startPOST the challenge + your credential; read ?code= from the link.Verify your credential live; mint a single-use, 60 s, PKCE-bound code; return the link.
3 · hand-offOpen bmdev://auth/federation?code=<C>#fed_verifier=<V>.The OS routes the deep link into Busymate DevTools.
4 · /exchange— (you never call it).Read code + verifier, re-derive S256(verifier), match the stored challenge, mint a real session, sign in — switching the account in place.

Reference implementations to copy

  • Web — the framework-free drop-in /federation-example/federation-onetap.js (no dependencies, no build step): set CLIENT_ID + BROKER_BASE at the top and call openInBusymateDevTools({ credential }) on your sign-in button. It inlines exactly the four steps above (its PKCE transform is byte-identical to the broker's).
  • iOS — the single-file drop-in FederationFlow.swift from the BMFederationExample sample app (Foundation + CryptoKit only, no third-party dependencies, no build step — ask the Busymate team for the sample app): copy this one file into your app, replace the three FederationConfig.partner constants at the top (clientID + brokerBase + partnerBaseURL), and call the hand-off on your sign-in button. It is the iOS analog of the web snippet — PKCE (verifier + S256 challenge, byte-identical to the broker's transform), FederationHandoff (build the bmdev://…#fed_verifier=… link), brokerStart (the /start call). Same "set 2-3 constants + call" ergonomics.

Why this is safe — the same-device PKCE model

The security rests on three properties, the same proven model the Busymate web dashboard uses for its own federated login (the #fed_verifier precedent):

  • The verifier never leaves the device. It rides the URL fragment (#…), which the OS keeps client-side. It is never sent to a server, never logged, never in a Referer header. The partner app holds it and passes it to Busymate DevTools on-device only.
  • The code alone is worthless. /start returns only a code — never a token. /exchange will not mint a session without the matching code_verifier, because the broker re-derives S256(verifier) and compares it to the stored code_challenge. A leaked / replayed link can't carry the secret fragment back to a server, so it can't be redeemed.
  • The code is single-use and short-lived. It is consumed atomically on first /exchange (a replay returns 410) and expires after 60 seconds.
  • The vouch is a verified session. At /start the broker checks your real credential — for directus_bearer, the identity comes from a live /users/me call, not from your request body — so you can only hand off a user your backend actually has signed in.

The credential and password are never placed in any URL; only the single-use, code-bound, 60-second verifier rides the (server-invisible) fragment.

Onboard a partner — one admin action

There is no per-partner code on Busymate's side. The broker is generic; everything specific to your app lives in one federation_clients row. A Busymate operator (admin) creates it once via the dashboard's Settings → API → Federation surface (/api?section=federation) or the equivalent MCP tool:

jsonc
upsert_federation_client {
  "client_id": "bm_yourapp",            // must be bm_<slug>  (^bm_[a-z0-9_]+$)
  "client_name": "Your App",
  "verify_mode": "directus_bearer",     // or "hmac"
  "directus_base_url": "https://your-backend.example",   // for directus_bearer
  // OR, for hmac:
  // "mint_hmac_secret": true,          // generates a secret, returns it ONCE, stores it value-blind in Vault
  "capped_role": "viewer"               // least-privilege clamp on every provisioned user
}

That's the whole server-side onboarding. From then on your app uses client_id: "bm_yourapp" in /start, and the broker drives the rest from this row — verify_mode, the trust anchor, the capped_role clamp, the per-client rate limits, and the enabled kill switch. The HMAC secret value (if any) is stored value-blind in Vault and never read back on any surface. The full field list lives on that Settings page; writes are admin-gated (federation:edit + admin + confirm).

By default, when a user's verified real email is free (nobody else owns it), they're provisioned under it; when it collides with an existing Busymate account, they get a fresh synthetic-addressed account instead — a federated login never silently takes over a pre-existing account. A separate, off-by-default email_linking_trusted flag on the same form opts a client into a guarded exception: on that same collision, the login may instead sign the user straight into their existing account (never an admin account, never another partner's federated account). See Federate a Supabase userbase for the full guard list — it applies the same way regardless of your client's verify_mode.

The reference implementations to copy

Two canonical, copy-paste drop-ins — one per client platform — both implementing exactly the four steps above. Each is the "set 2-3 constants + call on your sign-in button" artifact for its platform; pick the one for your client.

iOS — copy FederationFlow.swift

The iOS drop-in is the single file FederationFlow.swift from the BMFederationExample sample app (ask the Busymate team for it) — the iOS analog of the web federation-onetap.js. Copy this one file into your app. It is Foundation + CryptoKit only (no third-party dependencies, no build step, iOS 16.0+) and contains the whole integration: PKCE (the verifier + S256 challenge, byte-identical to the broker's transform), FederationHandoff (build bmdev://auth/federation?code=<C>#fed_verifier=<V>), and brokerStart (the /start call).

Replace the three FederationConfig.partner constants at the top with your values (the same two-line ergonomics as the web snippet's CLIENT_ID + BROKER_BASE):

swift
// At the top of FederationFlow.swift — the ONLY values a partner edits:
static let partner = FederationConfig(
    clientID:       "bm_yourapp",                                          // your registered client_id
    brokerBase:     "https://api.busymate.net/functions/v1/federation-broker",  // leave the default
    partnerBaseURL: "https://your-backend.example"                         // YOUR backend's sign-in base
)

Then call the hand-off on your sign-in button:

swift
let flow = FederationFlow()                         // uses FederationConfig.partner
let bearer = try await flow.directusLogin(email: e, password: p).accessToken  // YOUR backend session
let verifier  = PKCE.makeCodeVerifier()
let challenge = PKCE.challengeS256(for: verifier)
let code = FederationHandoff.codeFromLink(
    try await flow.brokerStart(codeChallenge: challenge, directusToken: bearer).link)!
await UIApplication.shared.open(FederationHandoff.url(code: code, verifier: verifier)!)
// → Busymate DevTools opens, redeems the code + verifier, and signs the user in.

The full runnable example wraps these calls in one method — the sample app's FederationViewModel.openInBusymateDevTools() — mint PKCE → /start → open the hand-off link. The BMFederationExample demo app is a minimal standalone SwiftUI app exposing exactly the single "Open in Busymate DevTools" button this page describes (logged in → one tap; not logged in → show login → the same one button). The defaults shipped in FederationConfig.partner are the demo's real tester values, so the example builds + runs as-is — a partner just overrides the three constants.

Web — copy federation-onetap.js

The web drop-in is /federation-example/federation-onetap.js (framework-free, no dependencies, no build step): set CLIENT_ID + BROKER_BASE at the top and call openInBusymateDevTools({ credential }) on your sign-in button.

Both drop-ins are byte-identical to the broker's PKCE transform (base64urlSha256), so a challenge minted by either is accepted at /start and re-derived at /exchange. A partner on any other platform reproduces the same four HTTP / deep-link steps in its own language.

Where this fits

  • The broker is the hosted federation service behind https://api.busymate.net/functions/v1/federation-broker — it owns the PKCE verification, the single-use code registry, and the real-session mint. There is no per-partner code in it.
  • The Busymate DevTools side parses the bmdev://auth/federation deep link (?code from the query, fed_verifier from the fragment) and redeems it with POST /exchange {code, code_verifier}.
  • The client registry is managed in Settings → API → Federation on the dashboard, mirrored by the list_federation_clients / upsert_federation_client / delete_federation_client MCP tools.

For the complete endpoint reference, error tables, account-provisioning policy, and the security model, ask the Busymate team.