stubkit docs

Setup · Required for entitlement reads

Tenant JWT

When a mobile/web app asks stubkit “is this user Pro?”, stubkit needs to know which user is asking and trust that the SDK isn’t lying about their identity. You do this by signing a short-lived JWT with your own private key. Stubkit verifies it against the public key you publish.

Two setup options

Option A — JWKS URL (recommended)

Host a standard JWKS endpoint at your own domain. Any OIDC/OAuth2 provider already does this:

  • Supabase: https://<project>.supabase.co/auth/v1/.well-known/jwks.json
  • Clerk: https://<frontend-api>.clerk.accounts.dev/.well-known/jwks.json
  • Auth0: https://<tenant>.auth0.com/.well-known/jwks.json
  • Firebase: https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com
  • Your own Worker: publish { "keys": [{ "kty": "RSA", "n": "...", "e": "AQAB", "kid": "..." }] }

Configure in stubkit:

  1. Dashboard → your app → Settings
  2. Under Tenant JWT config, paste the JWKS URL.
  3. Leave Issuer / Audience blank unless your tokens set them — in which case fill both to tighten the check.
  4. Save.

Option B — Issuer + Audience with fixed secret

Only use this if you’re not running any OIDC provider. Publish your public key bytes at a URL stubkit can fetch, and set the iss / audclaims your backend will emit.

Token requirements

Every token you mint must:

  • Be signed with RS256 (or ES256 if your JWKS says so).
  • Include a sub claim — your internal user id. This is what the SDK passes as userId.
  • Have an exp no more than 1 hour in the future. Refresh before it expires.
  • If you configured issuer/audience in stubkit, match them in iss / aud.

Example payload stubkit expects:

{
  "sub": "user_abc123",
  "iss": "https://auth.yourapp.com",
  "aud": "stubkit",
  "exp": 1770000000,
  "iat": 1769996400
}

Minting tokens — Node example

import jwt from 'jsonwebtoken';
import { readFileSync } from 'fs';

const privateKey = readFileSync('/run/secrets/jwt-private-key.pem');

export function mintStubkitToken(userId: string) {
  return jwt.sign(
    {
      sub: userId,
      iss: 'https://auth.yourapp.com',
      aud: 'stubkit',
    },
    privateKey,
    { algorithm: 'RS256', expiresIn: '1h', keyid: 'key-1' }
  );
}

Then in the mobile/web app, your backend exposes POST /stubkit-tokenwhich returns the signed JWT. The SDK’s getAuthToken callback calls that endpoint.

SDK wiring

import { StubkitClient } from '@stubkit/js';

const stubkit = new StubkitClient({
  appId: 'your-app-id',
  publishableKey: 'pk_live_xxx...',
  getAuthToken: async () => {
    const res = await fetch('/api/stubkit-token');
    const { token } = await res.json();
    return token;
  },
});

Cache behavior

Stubkit caches JWKS fetches in KV for 10 minutes. If you rotate your signing key and ship new kid-tagged tokens, stubkit will re-fetch within 10 min automatically. For emergency rotation, restart your signing infra and rely on the cache-miss on the next request.

Common errors

  • 401 jwt_missing — no Authorization header.
  • 401 jwt_expiredexp in the past; your minter TTL is too short or clock skew.
  • 401 jwt_invalid_signaturekid doesn’t resolve to your JWKS, or the key rotated and cache is stale.
  • 401 jwt_invalid_issuer / jwt_invalid_audience — claim mismatch with what you configured.