Integrations
Web SPA

Web SPA

Three solid options depending on how much control you want.

Option A — React widget (fastest)

If React is your stack and a popup login is acceptable, the React SDK is ~10 lines:

<CamelAuthProvider config={{ issuer: "https://accounts.camelcreatives.com", clientId: "your-app-web" }}>
  <CamelLoginButton onClick={login} logo="/logo.jpeg" />
</CamelAuthProvider>

Pass logo to show your Camel Accounts mark (or your own) on the left of the button; omit it to use the built-in camel mark.

Option B — oidc-client-ts (redirect flow)

Full redirect-based OIDC integration with automatic discovery:

npm install oidc-client-ts
// auth.ts
import { UserManager, WebStorageStateStore } from "oidc-client-ts";
 
export const userManager = new UserManager({
  authority: "https://accounts.camelcreatives.com",
  client_id: "your-app-web",
  redirect_uri: "https://yourapp.com/callback",
  scope: "openid profile email",
  response_type: "code",
 
  userStore: new WebStorageStateStore({ store: sessionStorage }),
  automaticSilentRenew: false,   // see note below — refresh via refresh_token instead
  loadUserInfo: true,            // fetches /oauth/userinfo after sign-in
});
 
export async function signIn() {
  await userManager.signinRedirect(); // builds PKCE + state for you
}
 
export async function finishSignIn() {
  const user = await userManager.signinRedirectCallback();
  return user; // .profile contains sub/name/email…, .access_token ready to use
}

Router wiring (any framework):

// /callback route
const user = await finishSignIn();
saveRefreshToken(user.refresh_token); // you keep this; userManager keeps its own copy too
router.push("/");

Calling an API with proactive refresh:

let cached: { access: string; expiresAt: number } | null = null;
 
export async function getAccessToken(): Promise<string> {
  if (cached && Date.now() < cached.expiresAt - 30_000) return cached.access;
  const user = await userManager.getUser();
  const res = await fetch("https://accounts.camelcreatives.com/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: user!.refresh_token!,
      client_id: "your-app-web",
    }),
  });
  if (!res.ok) { await userManager.removeUser(); throw new Error("re-login required"); }
  const t = await res.json();
  cached = { access: t.access_token, expiresAt: Date.now() + t.expires_in * 1000 };
  return t.access_token;
}

Why not automaticSilentRenew? Silent renew drives /authorize inside a hidden iframe (prompt=none). Camel Accounts' authorize endpoint doesn't implement prompt=none, so iframe renewals can fail or pop real UI. The refresh-token grant is the reliable renewal path here.

Option C — Vanilla JS

No library at all: follow Quick Start, which includes a complete single-file HTML demo (~80 lines).

Framework notes

  • Next.js: run Options A–C in client components ("use client") — the flow is browser-side by design. Your Route Handlers / Server Actions verify incoming bearer tokens server-side (Backend verification).
  • Vue/Svelte/Angular: use Option B's pattern with the framework's router callback page; oidc-client-ts is framework-agnostic.
  • Dev mode: register http://localhost:3000/callback as an extra redirect URI on your client — multiple URIs per client are fine, and exact matching applies to whichever one you send.

CORS

Browser calls to $ISSUER/oauth/token and $ISSUER/oauth/userinfo from another origin need your origin in the server's CORS_ALLOWED_ORIGINS. Preflights (OPTIONS) are answered with credentials allowed and a 24-hour preflight cache. If you get opaque network errors in dev, ask the operator to add http://localhost:3000 to that list.