A middleware API that turns Runn's account-level API token into safe, per-user timesheet access.
Runn's API v1 (https://api.runn.io) only issues account-level tokens, created by admins under Settings → API. A token is tied to the account, not to a user — so any write-scoped token can modify anyone's timesheets. There is no personal access token, and none is on Runn's public roadmap.
We still want individual users (starting with: us) to automate their own timesheet submission without ever holding the privileged token. The answer is a small token broker: a service that holds the one privileged Runn token server-side and issues its own scoped, per-user tokens on top.
Fig 1 — Three trust zones. The privileged Runn token exists only inside the broker.
The broker is not a transparent proxy. It exposes its own small API (/my/…) and translates each call into a specific, constrained Runn API call. Clients cannot reach arbitrary Runn endpoints through it.
Fig 2 — Happy path for a timesheet submission.
Keep it deliberately tiny. Every endpoint maps to one constrained upstream operation.
| Broker endpoint | Upstream (Runn v1) | Constraint applied |
|---|---|---|
POST /my/timesheet |
POST /actuals/timeentry |
personId forced from token; date range limited (e.g. current ± 1 period) |
POST /my/timesheet/bulk |
POST /actuals/bulk/ |
All entries rewritten to caller's personId; ≤ 100 entries; broker paces same-day entries |
GET /my/actuals?from&to |
GET /actuals |
Response filtered to caller's personId before returning |
GET /my/assignments |
GET /assignments |
Filtered to caller; used to know what projects/roles are valid targets |
GET /my/projects |
GET /projects (+ cache) |
Only projects the caller is assigned to; names/ids only, no financials |
Anti-pattern: a generic /proxy/* route that forwards paths to api.runn.io. That hands every client the full power of the admin token, including other people's data and financials. Never build this.
| Runn account token | Broker user token | |
|---|---|---|
| Issued by | Runn admin (Settings → API) | Broker admin endpoint / CLI |
| Lives in | Secrets manager on the broker host only | Client machines (CLI config, keychain) |
| Scope | Whole account (write) | One personId, allowlisted endpoints |
| Format | LIVE_… / TEST_… | Signed JWT: sub, personId, scope, exp, jti |
| Rotation | Set expiry at creation; rotate manually | Short-lived (e.g. 30–90 days) + revocation list by jti |
# admin-only, on the broker host
$ broker-admin issue --email jan@company.com --person-id 4711 \
--scope timesheet:write,timesheet:read --ttl 60d
→ eyJhbGciOiJIUzI1NiIs… (hand to the user once, store hash of jti)
The mapping email → personId is resolved once at issue time (via GET /people) and baked into the token + a server-side record. The client never supplies it.
personId in a request body is ignored; if present and different, reject with 403 and log it.ts, sub, jti, route, personId, upstream_status. This is also what makes your Runn admin comfortable issuing the account token.These live in the broker's upstream client so no consumer ever has to know about them:
429 Too many requests. The broker queues and spaces same-key writes.Accept-Version: 1.0.0.null / "" / [].TEST_ token (Runn's test account) via config until the flow is proven; flip to LIVE_ by env var only.jti, sub, personId, scope, exp, revoked_at.Bottom line: Runn gives you one big key. The broker turns it into many small keys — each opening exactly one person's timesheet drawer, with a camera pointed at the lock.