@wocha/react
v0.1.0
Published
React hooks and headless components for Wocha authentication
Maintainers
Readme
@wocha/react
React hooks and headless components for Wocha authentication in single-page applications. Implements the OAuth 2.0 authorisation code flow with PKCE as a public client — no client secret required.
Install
npm install @wocha/react
# or: pnpm add / yarn add / bun add @wocha/reactPeer dependency: React 18+.
Quick start
Wrap your app in WochaProvider and mount a /callback route for the OAuth redirect:
import { WochaProvider } from "@wocha/react";
const config = {
issuer: "https://my-tenant.auth.wocha.ai",
clientId: "your-client-id",
redirectUri: window.location.origin + "/callback",
};
export function App({ children }: { children: React.ReactNode }) {
return <WochaProvider config={config}>{children}</WochaProvider>;
}Register your redirect URI (https://your-app.com/callback) in the Wocha Console before testing.
Configuration reference
Pass a WochaAuthConfig object to WochaProvider:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| issuer | string | Yes | OIDC issuer URL (e.g. https://my-tenant.auth.wocha.ai). Used for authorisation, token, and logout endpoints. |
| clientId | string | Yes | OAuth client ID for your SPA application. |
| redirectUri | string | Yes | Callback URL registered with your OAuth client (e.g. https://app.example.com/callback). |
| scope | string | No | Space-separated scopes. Default: openid profile email org_id offline_access. |
| audience | string | No | API audience claim, if your tenant requires it. |
| apiUrl | string | No | Platform API base URL for permission checks and org switching. Defaults to {tenant}.api.wocha.ai when the issuer matches Wocha Cloud (*.auth.wocha.ai). |
| onRedirectCallback | (appState?: unknown) => void | No | Called after a successful OAuth callback. Receives any appState passed to login(). |
Hooks
All hooks must be used inside WochaProvider.
useSession()
Returns the current session state without throwing. Prefer this over useUser().
const { data, status, error, refresh } = useSession();
// data: GreetUser | null — the authenticated user, not a session object (unlike @wocha/nextjs)
// status: "loading" | "authenticated" | "unauthenticated"
// error: Error | null — last auth error (e.g. token refresh failure), null when none
// refresh: () => Promise<void> — re-fetch / refresh the access tokenReturn type: { data: GreetUser | null; status: "loading" | "authenticated" | "unauthenticated"; error: Error | null; refresh: () => Promise<void> }.
useUser()
Deprecated — use
useSession()instead.useUser()remains exported for backwards compatibility but may be removed in a future major version.
Returns the current user and auth status without throwing.
const { user, status } = useUser();
// user: GreetUser | null — { id, email, name?, orgId?, orgIds?, tenantId?, role?, emailVerified? }
// status: "loading" | "authenticated" | "unauthenticated"Return type: { user: GreetUser | null; status: "loading" | "authenticated" | "unauthenticated" }.
useOrg()
Organisation context and switching.
const { orgId, orgIds, switchOrg, isLoading } = useOrg();
// orgId: string | undefined
// orgIds: string[] | undefined
// switchOrg: (id: string) => Promise<void>
// isLoading: boolean — true while an org switch is in progressusePermission(check)
Checks a SpiceDB permission via the Platform API. Requires apiUrl (or a Wocha Cloud issuer) to be configured.
const { allowed, isLoading, error } = usePermission({
resource: { type: "document", id: "doc-123" },
permission: "edit",
// subject defaults to the current user
});Return type: { allowed: boolean; isLoading: boolean; error: Error | null }.
useAccessToken()
Low-level access to the current access token and refresh state.
const { accessToken, getAccessToken, isRefreshing } = useAccessToken();
// accessToken: string | null — cached access token (may be null before first refresh)
// getAccessToken: () => Promise<string | null> — returns a valid token, refreshing if needed
// isRefreshing: boolean — true while a silent refresh is in progressUse this when calling your own APIs with a bearer token. For most UI work, useSession() is sufficient.
useWochaAuth()
Low-level access to auth actions and state.
const { isAuthenticated, isLoading, user, login, logout, getAccessToken, error } =
useWochaAuth();login(options?)— redirects to the Wocha sign-in page. Pass{ signup: true }for registration, or{ appState }to round-trip custom state.logout()— clears local tokens and redirects to the Wocha logout endpoint.getAccessToken()— returns a valid access token, refreshing silently if needed.
Components
All components are headless: provide a render-prop children function for full control, or use the built-in defaults.
@wocha/react exports SignIn / SignUp (and SignInButton / SignUpButton as aliases). @wocha/nextjs uses the *Button names by default with SignIn / SignUp / SignOut as aliases — pick whichever convention fits your codebase.
SignIn / SignInButton
<SignIn>
{({ login }) => <button onClick={() => login()}>Log in</button>}
</SignIn>SignUp / SignUpButton
<SignUp>
{({ signup }) => <button onClick={() => signup()}>Create account</button>}
</SignUp>UserButton
Renders nothing while loading or unauthenticated.
<UserButton>
{({ user, logout }) => (
<div>
<span>{user.name ?? user.email}</span>
<button onClick={logout}>Sign out</button>
</div>
)}
</UserButton>OrgSwitcher
Renders nothing when the user has one or fewer organisations.
<OrgSwitcher>
{({ orgId, orgIds, switchOrg, isLoading }) => (
<select
value={orgId}
disabled={isLoading}
onChange={(e) => switchOrg(e.target.value)}
>
{orgIds?.map((id) => (
<option key={id} value={id}>{id}</option>
))}
</select>
)}
</OrgSwitcher>Authenticated / Unauthenticated
Conditional rendering based on auth state. Both accept an optional fallback prop shown while loading.
<Authenticated fallback={<Spinner />}>
<Dashboard />
</Authenticated>
<Unauthenticated>
<SignIn />
</Unauthenticated>Note: Authenticated renders null when the user is signed out — for protected routes, prefer an explicit guard with useSession() and <Navigate /> (see Protected routes below).
Protect
Permission-gated wrapper. Renders children only when the user is authenticated and passes the permission check. Renders fallback (or null) while loading or when denied.
<Protect
permission={{ resource: { type: "document", id: "doc-123" }, permission: "edit" }}
fallback={<p>You don't have access.</p>}
>
<EditForm />
</Protect>Requires apiUrl (or a Wocha Cloud issuer) in WochaProvider config — same as usePermission.
Self-hosted configuration
For self-hosted Wocha installations, set issuer to your auth server URL and explicitly provide apiUrl:
const config = {
issuer: "https://auth.internal.example.com",
clientId: process.env.VITE_WOCHA_CLIENT_ID!,
redirectUri: window.location.origin + "/callback",
apiUrl: "https://api.internal.example.com",
};Without apiUrl, permission checks and org switching are unavailable unless the issuer matches the Wocha Cloud pattern.
Security model
- Public client (PKCE): SPAs cannot safely store a client secret. This SDK uses PKCE (
S256) for the authorisation code exchange. - Token storage: Refresh tokens are stored in
sessionStorage(cleared when the tab closes). Access tokens are kept in memory and refreshed automatically before expiry. - Multi-tab sync: Logging in, out, or switching orgs in one tab broadcasts to other open tabs via
BroadcastChannel, keeping session state in sync. Tabs also refresh near-expiry tokens when they become visible again. - No server-side session: All token handling happens in the browser. Do not use this SDK in server-rendered contexts where secrets could leak — use
@wocha/nextjsinstead.
Common patterns
Protected routes
function ProtectedPage() {
const { status } = useSession();
if (status === "loading") return <Spinner />;
if (status === "unauthenticated") return <Navigate to="/login" />;
return <Dashboard />;
}Or use the Authenticated component:
<Route
path="/dashboard"
element={
<Authenticated>
<Dashboard />
</Authenticated>
}
/>Organisation switching
function OrgPicker() {
const { orgId, orgIds, switchOrg, isLoading } = useOrg();
if (!orgIds || orgIds.length <= 1) return null;
return (
<select
value={orgId}
disabled={isLoading}
onChange={(e) => void switchOrg(e.target.value)}
>
{orgIds.map((id) => (
<option key={id} value={id}>{id}</option>
))}
</select>
);
}Some org switches require re-authorisation; switchOrg handles the redirect automatically when needed.
Permission gating
function EditButton({ documentId }: { documentId: string }) {
const { allowed, isLoading, error } = usePermission({
resource: { type: "document", id: documentId },
permission: "edit",
});
if (isLoading) return null;
if (error) return null;
if (!allowed) return null;
return <button>Edit</button>;
}Related packages
| Package | Use when |
|---------|----------|
| @wocha/nextjs | Next.js App Router with server-side sessions |
| @wocha/sdk | Server-side user/org management via the Management API |
| @wocha/cli | Scaffold auth integration with npx @wocha/cli init |
Troubleshooting
redirect_uri_mismatch
The redirectUri in WochaProvider must exactly match a redirect URI registered in the Wocha Console (scheme, host, port, and path). Typical SPA value: window.location.origin + "/callback".
CSRF / state_mismatch
PKCE state is stored in browser storage for the duration of the login redirect. If you see state errors after a failed login, clear storage for your origin and try again. Do not start a second login flow in another tab before the first completes.
CORS errors
The token endpoint must allow your SPA origin. Register the correct redirect URI in the Console; if problems persist on a custom domain, confirm the OAuth application allows that origin for browser-based clients.
Storage cleared between sessions
By default tokens and refresh state use sessionStorage. If the user clears site data or uses a private window that ends, they must sign in again. Use storage: "local" in config only if you accept the trade-off of longer-lived tokens in the browser.
Environment variable prefix
Vite requires VITE_-prefixed variables (VITE_WOCHA_ISSUER, VITE_WOCHA_CLIENT_ID). Create React App uses REACT_APP_. Values are baked in at build time — restart the dev server after changing .env.
