Client Abilities Token Authorization
Short-lived JWTs for UI permission gating per herd
Client Abilities Tokens
Short-lived JWTs carry per-herd ability keys. Abilities keys can be used to gate specific actions. For example, before allowing "user x" to upload a photo, you can first check if "user x" has the corresponding upload photo ability.
Claims
| Claim | Role |
|---|---|
sub | User UUID |
platform_superadmin | When true, UI may allow all abilities |
herds | [{ herd_id, slug, abilities: string[] }, …] per active membership |
aud | scout-client |
iat / exp | Issued / expiry (CLIENT_ABILITIES_TOKEN_TTL_SEC; edge default 900s if unset, production often 3600) |
Verifiers use public JWKs by kid. Edge secrets are: CLIENT_ABILITIES_JWT_PRIVATE_JWK and optional CLIENT_ABILITIES_TOKEN_TTL_SEC (within range of 60–86400 seconds).
Flow
- Client calls
mint-client-abilities-token. - Edge runs
get_client_abilities_token_claims()as the caller and signsherds/platform_superadmin. - Client gates actions, visuals, and realtime channel joins based on abilities.
- All supabase requests still enforce permissions via RLS (including
realtime.messagesfor private channels). The abilities JWT cannot replace Realtime RLS — it uses a different signing key and audience than the Supabase session JWT.
Who Gets What Claims
| Caller | Claims | Mint |
|---|---|---|
| Platform superadmin | platform_superadmin: true, herds: [] | Yes |
| User with active herd role(s) | One herd entry per membership, abilities from role matrix | Yes |
| User, no active herds | — | No (null → 403) |
| Device API key / anon | — | No |
| Unauthenticated | — | No |
Database
| Object | Role |
|---|---|
client_abilities_jwt_public_keys | Active public JWKs by kid |
get_client_abilities_jwt_public_keys() | Keys for verifiers |
upsert_client_abilities_jwt_public_key | Register / rotate (service role or superadmin) |
revoke_client_abilities_jwt_public_key | Soft-revoke |
get_client_abilities_token_claims() | Mint only — not for clients |
Rotation: upsert public key → update edge private JWK → wait TTL → revoke old kid. Refresh tokens when membership or role matrices change.
Generate a key pair
deno eval "
import { generateKeyPair, exportJWK } from 'npm:jose@5';
const kid = 'client-abilities-' + crypto.randomUUID().slice(0, 8);
const { publicKey, privateKey } = await generateKeyPair('ES256', { extractable: true });
const pub = await exportJWK(publicKey);
const priv = await exportJWK(privateKey);
pub.kid = kid;
priv.kid = kid;
console.log('# Private (EDGE ONLY):\n');
console.log(JSON.stringify(priv));
console.log('\n# Public:\n');
console.log(JSON.stringify(pub));
"