
Security News
Socket for Origin Now Available
Socket for Origin scans dependency changes in pull requests and flags security risks based on your team’s policies. Now available in Cursor’s App Marketplace.
webba-id-sdk
Advanced tools
Official SDK for OAuth 2.0 + PKCE authentication with Webba ID — frontend, backend, and bots.
Official SDK for OAuth 2.0 + PKCE authentication with Webba ID.
npm install webba-id-sdk
If you're using Claude Code, also run:
npx webba-id-sdk init
It installs the /webba-id skill at .claude/skills/webba-id/ (so the AI agents in your project know how the SDK actually works) and generates AuthContext.tsx + AuthCallback.tsx templates.
// src/main.tsx
import { webba } from 'webba-id-sdk'
webba.configure({
appKey: import.meta.env.VITE_WEBBA_APP_KEY, // wba_xxx
appSecret: import.meta.env.VITE_WEBBA_APP_SECRET, // wbs_xxx — see security note below
redirectUri: window.location.origin + '/auth/callback',
storagePrefix: 'myapp', // localStorage keys: myapp_tokens / myapp_user
})
// Login button
import { webba } from 'webba-id-sdk'
<button onClick={() => webba.login()}>Sign in with Webba ID</button>
// /auth/callback page
import { useEffect, useRef } from 'react'
import { webba } from 'webba-id-sdk'
export function AuthCallback() {
const ran = useRef(false)
useEffect(() => {
if (ran.current) return
ran.current = true
webba.handleCallback().then(async (tokens) => {
const user = await webba.getUser(tokens.accessToken)
webba.saveTokens(tokens)
webba.saveUser(user)
location.href = '/'
})
}, [])
return <p>Connecting...</p>
}
That's a full working integration. For a production-quality AuthContext (token refresh, cross-tab sync, focus listeners), copy the template the init CLI installs at src/contexts/AuthContext.tsx.
appSecret and VITE_*The Webba ID /auth-token endpoint requires the app secret to exchange a code for tokens. There is no PKCE-only flow yet on the server — meaning a true public SPA cannot exchange codes without revealing the secret somewhere.
You have two choices:
Keep your secret server-side. The frontend handles login/state/PKCE and ships the code to your backend; your backend calls Webba ID with the secret.
// Frontend (no secret)
webba.configure({
appKey: import.meta.env.VITE_WEBBA_APP_KEY,
redirectUri: location.origin + '/auth/callback',
})
// Backend (Express, Edge Function, ...)
import { createWebbaID } from 'webba-id-sdk'
const server = createWebbaID({
appKey: process.env.WEBBA_APP_KEY!,
appSecret: process.env.WEBBA_APP_SECRET!, // server-only env, NO `VITE_` prefix
redirectUri: 'https://example.com/auth/callback',
})
const tokens = await server.exchangeCode(code, codeVerifier)
If your app's threat model is fine with shipping the app secret to clients (low-stakes apps, internal tools), put it in VITE_WEBBA_APP_SECRET and call webba.handleCallback() directly.
⚠ NEVER use
VITE_*for secrets you actually want kept private. Vite inlinesVITE_*env vars into the production bundle — anyone hitting the page sees the value in DevTools.
interface WebbaIDConfig {
appKey: string // wba_xxx — REQUIRED
appSecret?: string // wbs_xxx — required for token exchange and reverse lookup
redirectUri?: string // defaults to `${origin}/auth/callback`
apiKey?: string // Webba ID Supabase anon key — defaults to production
baseUrl?: string // defaults to https://account.webba-creative.com
apiUrl?: string // defaults to the production Edge Functions URL
storagePrefix?: string // localStorage prefix — defaults to 'webba'
}
import { webba } from 'webba-id-sdk'
// Reads VITE_WEBBA_APP_KEY, VITE_WEBBA_APP_SECRET, VITE_WEBBA_REDIRECT_URI,
// VITE_WEBBA_API_KEY (and the non-prefixed names on Node).
const ok = webba.configureFromEnv()
if (!ok) {
// VITE_WEBBA_APP_KEY was missing
}
| Method | Description |
|---|---|
login() | Generate PKCE + state, redirect to Webba ID's /oauth/authorize |
isCallback() | True when the URL has ?code= or ?error= |
handleCallback() | All-in-one: parse URL, validate state, exchange code → AuthTokens. Idempotent under React StrictMode |
exchangeCode(code, codeVerifier?) | Lower-level: just exchange the code (server-side or advanced) |
| Method | Description |
|---|---|
refreshTokens(refreshToken) | Returns new AuthTokens |
revokeToken(token) | Revoke an access or refresh token |
verifyToken(accessToken) | { valid: boolean, user_id?, email? } (never throws) |
logout(accessToken?, refreshToken?) | Revoke + clear local state |
| Method | Description |
|---|---|
getUser(accessToken) | Convenience WebbaUser shape (id, email, name, avatar) |
getUserInfo(accessToken, includeProfile?) | Raw WebbaIDUser (with subscriptions when includeProfile=true) |
getProfile(accessToken) | Full profile + subscriptions + roles |
updateProfile(accessToken, partial) | PUT changes to the profile |
| Method | Description |
|---|---|
saveTokens(tokens) / getStoredTokens() / clearTokens() | localStorage at ${storagePrefix}_tokens |
saveUser(user) / getStoredUser() / clearUser() | localStorage at ${storagePrefix}_user |
isTokenExpired(tokens) | Date.now() >= tokens.expiresAt |
| Method | Description |
|---|---|
getLinkedIdentities(accessToken) | All linked OAuth providers |
hasProviderLinked(accessToken, provider) | Boolean shortcut |
getProviderIdentity(accessToken, provider) | Single identity or null |
getProviderId(accessToken, provider) | Just the provider id, or null |
hasCfxreLinked(accessToken) / getCfxreIdentity / getCfxreId | Cfx.re shortcuts |
appSecret)| Method | Description |
|---|---|
lookupUserByProvider(provider, providerUserId) | Find a Webba user from a provider id |
lookupUsersByProvider(provider, providerUserIds[]) | Batch (max 100) |
getUserIdentitiesByUserId(userId) | All providers linked to a Webba user |
getProviderIdByUserId(userId, provider) | Provider id for a user, or null |
getCfxreIdByUserId(userId) / getDiscordIdByUserId(userId) | Shortcuts |
| Method | Description |
|---|---|
checkAppAccess(accessToken, orgId?) | { has_access, reason, subscription, organizations } |
hasAccess(accessToken, orgId?) | Boolean shortcut |
enableAppForOrg(accessToken, orgId) / disableAppForOrg(...) | Toggle the app for an org |
import { webba } from 'webba-id-sdk'
webba.configure({ appKey: 'wba_xxx', appSecret: 'wbs_xxx', redirectUri: '...' })
await webba.login()
import { createWebbaID } from 'webba-id-sdk'
const client = createWebbaID({
appKey: process.env.WEBBA_APP_KEY!,
appSecret: process.env.WEBBA_APP_SECRET!,
redirectUri: 'https://example.com/callback',
})
// Discord bot example
const result = await client.lookupUserByProvider('discord', message.author.id)
if (result.user) {
console.log('Webba user:', result.user.user_id)
}
If two Webba apps share the same origin (e.g. an admin panel and a customer app on app.example.com), set distinct prefixes so their tokens don't collide:
webba.configure({ appKey, appSecret, redirectUri, storagePrefix: 'admin' })
// → admin_tokens / admin_user
Murphy's Workshop uses storagePrefix: 'murphy'. The default is 'webba'.
AuthContextThe basic example above misses cross-tab sync, proactive refresh on focus, and lock coordination. The init CLI ships a battle-tested template (~230 lines) that handles all of it. Read .claude/skills/webba-id/auth-context.md after running npx webba-id-sdk init, or check the source on the Webba ID repo.
import { createWebbaID } from 'webba-id-sdk'
const webbaId = createWebbaID({
appKey: process.env.WEBBA_APP_KEY!,
appSecret: process.env.WEBBA_APP_SECRET!,
redirectUri: 'https://example.com/auth/callback',
})
// Find a Webba user from a Discord user
const result = await webbaId.lookupUserByProvider('discord', '1234567890')
if (result.user) {
// Their Cfx.re id?
const cfxreId = await webbaId.getCfxreIdByUserId(result.user.user_id)
}
// Batch lookup
const batch = await webbaId.lookupUsersByProvider('discord', ['111', '222', '333'])
console.log(batch.mapping)
// { '111': 'webba-uuid-1', '222': 'webba-uuid-2' } // 333 not linked
Supported providers: discord, google, github, azure, cfxre.
You're likely using a self-hosted Webba ID and forgot to set apiKey. The SDK sends apikey + Authorization: Bearer <apiKey> headers on every request — if the value isn't a valid Supabase anon key for your Webba ID instance, Supabase's gateway rejects every call.
You called handleCallback() twice with the same code (e.g. React StrictMode double-mount, or you remounted the callback component). The SDK has internal protection against this, but check that your callback page guards with a useRef flag too:
const ran = useRef(false)
useEffect(() => {
if (ran.current) return
ran.current = true
webba.handleCallback().then(...)
}, [])
PKCE data is gone. Common causes:
login() and the callback.localhost vs 127.0.0.1).The exchange-code call hit the server without an app secret. Either:
appSecret in webba.configure(...) (frontend; ships secret in bundle), orwebba is unknownMake sure you import from the package, not from a local file:
import { webba } from 'webba-id-sdk' // ✓
import { webba } from './webba-id-sdk' // ✗
1. User clicks "Sign in"
↓
2. webba.login() → generates PKCE pair + state
→ redirects to https://account.webba-creative.com/oauth/authorize
3. User authenticates on Webba ID
↓
4. Webba ID redirects to /auth/callback?code=...&state=...
↓
5. webba.handleCallback()
↓ verify state (CSRF)
↓ POST /auth-token { code, app_key, app_secret, redirect_uri, code_verifier }
↓ returns AuthTokens
6. webba.getUser(accessToken)
↓ GET /auth-userinfo?app_key=...&access_token=...&include_profile=true
↓ returns WebbaUser
7. webba.saveTokens(tokens) / webba.saveUser(user)
↓
8. Redirect to your app's home page
MIT — Webba Creative Technologies 2026
FAQs
Official SDK for OAuth 2.0 + PKCE authentication with Webba ID — frontend, backend, and bots.
We found that webba-id-sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Socket for Origin scans dependency changes in pull requests and flags security risks based on your team’s policies. Now available in Cursor’s App Marketplace.

Security News
Agent Baseline’s new reference architecture shows how to secure software factories with four boundaries enforced outside the AI agent.

Security News
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.