Webba ID SDK v2.1
Official SDK for OAuth 2.0 + PKCE authentication with Webba ID.

Installation
Copy src/lib/webba-id.ts into your project, or install via npm:
npm install webba-id-sdk
Required Configuration
Get your credentials at https://account.webba-creative.com/platform/applications
VITE_WEBBA_API_KEY=your_webba_id_supabase_anon_key
VITE_WEBBA_CLIENT_ID=wba_xxx
VITE_WEBBA_CLIENT_SECRET=wbs_xxx
VITE_WEBBA_REDIRECT_URI=http://localhost:5173/auth/callback
Usage
1. Configuration (main.tsx)
import { WebbaID } from '@/lib/webba-id'
WebbaID.configure({
apiKey: import.meta.env.VITE_WEBBA_API_KEY,
clientId: import.meta.env.VITE_WEBBA_CLIENT_ID,
clientSecret: import.meta.env.VITE_WEBBA_CLIENT_SECRET,
redirectUri: import.meta.env.VITE_WEBBA_REDIRECT_URI
})
2. Login (redirects to Webba ID)
async function handleLogin() {
await WebbaID.login()
}
3. Callback (/auth/callback)
import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { WebbaID } from '@/lib/webba-id'
export default function AuthCallback() {
const navigate = useNavigate()
const [error, setError] = useState<string | null>(null)
const processingRef = useRef(false)
useEffect(() => {
const processCallback = async () => {
if (processingRef.current) return
processingRef.current = true
if (!WebbaID.isCallback()) {
setError('Invalid callback page')
return
}
try {
const tokens = await WebbaID.handleCallback()
const user = await WebbaID.getUser(tokens.accessToken)
WebbaID.saveTokens(tokens)
WebbaID.saveUser(user)
navigate('/dashboard')
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
processingRef.current = false
}
}
processCallback()
}, [navigate])
if (error) return <div>Error: {error}</div>
return <div>Connecting...</div>
}
4. Using Tokens
const user = WebbaID.getStoredUser()
const tokens = WebbaID.getStoredTokens()
if (user && tokens && !WebbaID.isTokenExpired(tokens)) {
}
if (tokens && WebbaID.isTokenExpired(tokens)) {
try {
const newTokens = await WebbaID.refreshTokens(tokens.refreshToken)
WebbaID.saveTokens(newTokens)
} catch {
WebbaID.clearTokens()
}
}
5. Logout
async function handleLogout() {
const tokens = WebbaID.getStoredTokens()
await WebbaID.logout(tokens?.accessToken)
window.location.href = '/'
}
API Reference
Configuration
interface WebbaIDConfig {
apiKey: string
clientId: string
clientSecret?: string
redirectUri?: string
baseUrl?: string
apiUrl?: string
}
Methods
configure(config) | Configure the SDK (call once at startup) |
isConfigured() | Check if SDK is configured |
login() | Redirect to Webba ID for authentication |
handleCallback() | Handle OAuth callback and return tokens |
getUser(accessToken) | Get user info |
refreshTokens(refreshToken) | Refresh expired tokens |
logout(accessToken?) | Revoke token and clear storage |
isCallback() | Check if on callback page |
saveTokens(tokens) | Save tokens to localStorage |
getStoredTokens() | Get stored tokens |
clearTokens() | Remove stored tokens |
isTokenExpired(tokens) | Check if tokens are expired |
saveUser(user) | Save user to localStorage |
getStoredUser() | Get stored user |
Organizations
getOrganizations(accessToken) | List user's organizations |
getOrganization(accessToken, orgId) | Get organization details |
createOrganization(accessToken, data) | Create organization (becomes owner) |
updateOrganization(accessToken, orgId, data) | Update organization |
deleteOrganization(accessToken, orgId) | Delete organization |
getOrganizationMembers(accessToken, orgId) | List members |
inviteMember(accessToken, orgId, data) | Invite member by email |
removeMember(accessToken, orgId, memberId) | Remove member |
updateMemberRole(accessToken, orgId, memberId, role) | Change member role |
App Access
checkAppAccess(accessToken, orgId?) | Check if user has access (returns details) |
hasAccess(accessToken, orgId?) | Quick boolean check for access |
enableAppForOrg(accessToken, orgId) | Enable this app for an organization |
disableAppForOrg(accessToken, orgId) | Disable this app for an organization |
Types
interface AuthTokens {
accessToken: string
refreshToken: string
expiresAt: number
userId: string
}
interface WebbaUser {
id: string
email: string
name: string
firstName?: string
lastName?: string
avatar?: string
}
type OrgRole = 'owner' | 'admin' | 'sso_admin' | 'viewer' | 'member'
interface Organization {
id: string
name: string
slug: string
logo_url: string | null
is_active: boolean
created_at: string
role: OrgRole
members_count?: number
domains_count?: number
}
interface OrganizationMember {
id: string
organization_id: string
user_id: string | null
role: OrgRole
email?: string
profile?: {
first_name?: string
last_name?: string
display_name?: string
avatar_url?: string
}
}
interface CreateOrganizationData {
name: string
slug?: string
logo_url?: string
}
interface InviteMemberData {
email: string
role?: 'admin' | 'sso_admin' | 'viewer' | 'member'
}
interface AppAccessResult {
has_access: boolean
reason: 'subscription' | 'org_access' | 'subscription_and_org' | 'no_access'
user_id: string
app_id: string
subscription: {
id: string
plan: string
status: string
features: Record<string, any> | null
starts_at: string | null
ends_at: string | null
} | null
organizations: Array<{
id: string
name: string
slug: string
role: string
}>
}
OAuth Flow
1. User clicks "Login"
|
2. WebbaID.login() → Redirect to account.webba-creative.com
|
3. User authenticates on Webba ID
|
4. Redirect back to /auth/callback?code=xxx&state=xxx
|
5. WebbaID.handleCallback() → Exchange code for tokens
|
6. WebbaID.getUser() → Get user info
|
7. Save tokens & user → Redirect to dashboard
Security
PKCE (Proof Key for Code Exchange)
The SDK automatically uses PKCE to protect against interception attacks:
code_verifier randomly generated (32 bytes)
code_challenge = SHA-256(code_verifier) in base64url
- Stored in cookie + localStorage (cross-origin safe)
State Parameter
The SDK generates a random state for each login and verifies it on callback (CSRF protection).
Token Storage
- Tokens stored in localStorage (
webba_tokens, webba_user)
- PKCE data stored in cookie + localStorage with 10 minute expiration
- Automatic cleanup of old PKCE sessions
Troubleshooting
"Session expired or invalid"
PKCE data has expired or is not accessible. Possible causes:
- More than 10 minutes between login() and callback
- Cookies blocked by browser
- Different origin (e.g., 127.0.0.1 vs localhost)
Solution: Use the same origin (localhost) in development.
"Invalid or expired code"
The authorization code has already been used or expired.
Solution: Add useRef to prevent double execution in React StrictMode (see callback example above).
"clientSecret is required"
The clientSecret was not provided in configuration.
Solution: Add clientSecret: import.meta.env.VITE_WEBBA_CLIENT_SECRET to configure().
Organizations Example
import { WebbaID } from '@/lib/webba-id'
const tokens = WebbaID.getStoredTokens()
const orgs = await WebbaID.getOrganizations(tokens.accessToken)
console.log(orgs)
const newOrg = await WebbaID.createOrganization(tokens.accessToken, {
name: 'My Company',
slug: 'my-company'
})
const org = await WebbaID.getOrganization(tokens.accessToken, newOrg.id)
console.log(org.domains, org.members_count)
await WebbaID.updateOrganization(tokens.accessToken, org.id, {
name: 'New Name'
})
const members = await WebbaID.getOrganizationMembers(tokens.accessToken, org.id)
await WebbaID.inviteMember(tokens.accessToken, org.id, {
email: 'newuser@example.com',
role: 'member'
})
await WebbaID.updateMemberRole(tokens.accessToken, org.id, memberId, 'admin')
await WebbaID.removeMember(tokens.accessToken, org.id, memberId)
await WebbaID.deleteOrganization(tokens.accessToken, org.id)
App Access Example
Use this to control access to your application based on subscriptions or organization membership.
import { WebbaID } from '@/lib/webba-id'
const tokens = WebbaID.getStoredTokens()
const access = await WebbaID.checkAppAccess(tokens.accessToken)
if (!access.has_access) {
console.log('Access denied. Reason:', access.reason)
window.location.href = '/upgrade'
}
if (access.subscription) {
console.log('User plan:', access.subscription.plan)
}
if (access.organizations.length > 0) {
console.log('Organizations with access:', access.organizations)
}
if (await WebbaID.hasAccess(tokens.accessToken)) {
} else {
}
const orgAccess = await WebbaID.checkAppAccess(tokens.accessToken, 'org-uuid')
Access is granted if:
- User has an active subscription for this app, OR
- User is member of an organization where the app is enabled
Auto-enable for Organization
Apps can enable themselves for an organization when a user logs in:
import { WebbaID } from '@/lib/webba-id'
const tokens = WebbaID.getStoredTokens()
const orgs = await WebbaID.getOrganizations(tokens.accessToken)
if (orgs.length > 0) {
await WebbaID.enableAppForOrg(tokens.accessToken, orgs[0].id)
}
const access = await WebbaID.checkAppAccess(tokens.accessToken, orgs[0].id)
console.log(access.has_access)
AuthContext Example
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
import { WebbaID, AuthTokens, WebbaUser } from '@/lib/webba-id'
interface AuthContextType {
user: WebbaUser | null
tokens: AuthTokens | null
isLoading: boolean
login: () => Promise<void>
logout: () => Promise<void>
setAuth: (tokens: AuthTokens, user: WebbaUser) => void
}
const AuthContext = createContext<AuthContextType | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<WebbaUser | null>(null)
const [tokens, setTokens] = useState<AuthTokens | null>(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const storedTokens = WebbaID.getStoredTokens()
const storedUser = WebbaID.getStoredUser()
if (storedTokens && storedUser && !WebbaID.isTokenExpired(storedTokens)) {
setTokens(storedTokens)
setUser(storedUser)
}
setIsLoading(false)
}, [])
const login = async () => {
await WebbaID.login()
}
const logout = async () => {
await WebbaID.logout(tokens?.accessToken)
setUser(null)
setTokens(null)
}
const setAuth = (newTokens: AuthTokens, newUser: WebbaUser) => {
WebbaID.saveTokens(newTokens)
WebbaID.saveUser(newUser)
setTokens(newTokens)
setUser(newUser)
}
return (
<AuthContext.Provider value={{ user, tokens, isLoading, login, logout, setAuth }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) throw new Error('useAuth must be used within AuthProvider')
return context
}
License
MIT - Webba Creative Technologies 2026