Skip to content

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.

  • 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.

  • Sent via x-api-key: as a JWT, alongside or instead of Authorization: Bearer <userJwt>.
  • Verified by introspection, not locally: API tokens are signed with a per-app HS256 secret this package never holds, so bridge-express POSTs 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: null marks 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)
}

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 populates req.bridgeUser; the API key is treated as informational only. The request is rejected only if the API token is the only credential offered.

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.

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.

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.