API tokens
Section titled “API tokens”Bridge lets you offer your own users a self-service way to create API tokens for programmatic access to your API: the same idea as a GitHub or Stripe personal access token, without you having to build token issuance, storage, or revocation yourself.
An Express backend is on the verifying side of this: a caller sends the token, and bridge-express checks it on every request.
Use cases
Section titled “Use cases”- CI/CD and scripts: a user wires a token into a pipeline or cron job to call your API unattended.
- Personal automation: a power user scripts against your API for their own workflows (exports, syncs, bulk edits).
- Third-party integrations: a user hands a token to a tool they use (a BI dashboard, a Zapier-style integration) so it can read or write on their behalf without sharing their password.
None of these need a real login session; that’s exactly the gap API tokens fill.
How it works
Section titled “How it works”- Sent via
x-api-key: as a JWT, alongside or instead ofAuthorization: Bearer <userJwt>. - Verified by introspection, not locally: API tokens are signed with a per-app HS256 secret this package never holds, so
bridge-expressPOSTs the token to Bridge’s introspection endpoint ({apiBaseUrl}/account/api-token/introspect) rather than checking a signature itself. The Bridge collapses every rejection (forged, tampered, revoked, expired) into{ active: false }, so there’s no information leak about why a token was rejected. - Privilege-scoped: a token is created with an explicit set of privileges (the same privilege keys your roles use), picked from a searchable list. It can never do more than what it’s granted, and the middleware enforces that server-side (see below).
- Workspace-scoped: a token is bound to the workspace it was created in (a workspace is called a tenant in the API;
tenantId: nullmarks an app-level token not bound to any single workspace) and can’t be replayed against another one. - Hash-at-rest, shown once: Bridge stores only a salted hash. The full token value is shown exactly once, right after creation. Nothing about long-term storage or display is your app’s concern.
- Revocation: backend SDKs verify API tokens by asking Bridge (introspection) rather than checking a local signature, and by default they do this on every request, so a revoked token is rejected on its very next call. If your backend enables introspection-result caching (
introspectionCacheTtlMs, see Configuration), rejection can lag by up to that cache’s TTL.
On success, the claims are attached to req.bridgeApiToken:
interface ApiTokenClaims {
sub: string; // Token subject identifier
appId: string; // App ID the token was issued for
tenantId: string | null; // Tenant ID (null for app-level tokens)
type: 'api'; // Always 'api' for API tokens
privileges: string[]; // Privilege strings (e.g. ['USER_READ', 'TENANT_WRITE'])
exp?: number; // Expiry (epoch seconds)
}
Requiring a privilege
Section titled “Requiring a privilege”Pass privilege to bridge.protect(...) to require that an API token carries a specific privilege. User JWTs bypass this option entirely. It only applies to the API-token path, so adding a privilege requirement to an endpoint doesn’t break existing user-JWT access:
// API tokens must carry USER_READ; user JWTs are unaffected.
router.get('/users', bridge.protect({ privilege: 'USER_READ' }), handler);
// API tokens must carry USER_WRITE.
router.post('/users', bridge.protect({ privilege: 'USER_WRITE' }), handler);
Restricting which credential types an endpoint accepts
Section titled “Restricting which credential types an endpoint accepts”acceptAuth restricts which credential types an endpoint accepts. The type is 'jwt' | 'api_token' | 'both' (default 'both'):
// Only user JWTs accepted; an API token alone gets 401
bridge.protect({ acceptAuth: 'jwt' })
// Only API tokens accepted; a user JWT alone gets 401
bridge.protect({ acceptAuth: 'api_token' })
// Both accepted (default when omitted)
bridge.protect({ acceptAuth: 'both' })
When
acceptAuth: 'jwt'and both headers are present (some Bridge frontends always send both), the request is accepted and the JWT path populatesreq.bridgeUser; the API key is treated as informational only. The request is rejected only if the API token is the only credential offered.
Dual-auth endpoints
Section titled “Dual-auth endpoints”Endpoints that accept both user JWTs and API tokens (the default) branch on which context is present:
router.get('/users', bridge.protect({ privilege: 'USER_READ' }), (req, res) => {
if (req.bridgeApiToken) {
// Authenticated via API token
return res.json({ users: [], tenantId: req.bridgeApiToken.tenantId });
}
// Authenticated via user JWT
const user = req.bridgeUser!;
return res.json({ users: [], tenantId: user.tenantId });
});
Both credentials can be present and valid on the same request: req.bridgeApiToken and req.bridgeUser coexist rather than one overriding the other. See Request authentication states for the full outcome table.
API-token-only endpoints
Section titled “API-token-only endpoints”For machine-to-machine traffic only:
router.post(
'/integrations/sync',
bridge.protect({ acceptAuth: 'api_token', privilege: 'TENANT_WRITE' }),
(req, res) => {
const { tenantId, privileges } = req.bridgeApiToken!;
res.json({ synced: true, tenantId });
},
);
Using a token to call another Bridge-aware service
Section titled “Using a token to call another Bridge-aware service”If your Express app itself needs to call a downstream service on behalf of the caller (rather than just verifying an inbound token), forward the raw credential with bridge.http. See Getting the user token for the equivalent on the user-JWT path.
Revoke generously
Section titled “Revoke generously”Revoking a token is irreversible, but tokens are cheap to reissue, and a stale grant is a common way access leaks. When in doubt, revoke and mint a fresh one.
Letting your users manage their own tokens
Section titled “Letting your users manage their own tokens”Issuing, listing, and revoking API tokens is a management-plane concern, not something bridge-express exposes an API for. That flow lives in a frontend Bridge SDK’s drop-in token-management component, or the CLI / Control Center (your admin dashboard at app.thebridge.dev). Your Express app only ever sees the result: a token on x-api-key that it verifies on each request.