How to build candidate authentication for a job board

Connect registration, verification, login, explicit refresh, and logout.

Treat authentication as a state machine owned by your app. The SDK performs API operations and token persistence according to your configured storage.

Choose who owns storage

This browser example uses tab-scoped session storage. For an SSR application, use storage: 'nostore' and the SSR session cookie flow instead.

ts
// src/lib/cavuno.browser.ts
import { createBoardClient } from '@cavuno/board';
export const board = createBoardClient({
board: 'pk_example',
auth: { storage: 'session' },
});

Persistent browser modes are scoped by board identifier, so two boards on one origin do not share tokens. They remain readable by JavaScript and therefore require normal XSS defenses.

Register and verify a candidate

register stores the returned access and refresh tokens using the configured storage. The application owns the form, verification screen, and navigation.

ts
import { board } from './cavuno.browser';
export async function registerCandidate(input: {
email: string;
password: string;
displayName: string;
}) {
const session = await board.auth.register({
role: 'candidate',
method: 'emailpass',
...input,
});
return {
user: session.boardUser,
needsVerification: !session.boardUser.emailVerified,
};
}
export async function verifyCode(code: string) {
await board.auth.verifyEmailWithCode({ code });
return board.me.retrieve();
}

The emailed magic-link token can instead be sent to board.auth.verifyEmail({ token }). Do not confuse that anonymous token flow with verifyEmailWithCode, which requires the signed-in bearer.

Login, refresh, and logout

The SDK does not refresh automatically after a 401. Make one explicit refresh attempt, retry the protected read once, and return to signed-out state if rotation fails.

ts
import { isUnauthorized } from '@cavuno/board';
export async function login(email: string, password: string) {
await board.auth.login({ email, password });
return board.me.retrieve();
}
export async function loadCurrentUser() {
try {
return await board.me.retrieve();
} catch (error) {
if (!isUnauthorized(error)) throw error;
await board.auth.refresh();
return board.me.retrieve();
}
}
export async function logout() {
await board.auth.logout();
}

Refresh tokens are single-use. Avoid letting multiple browser requests independently refresh the same expired session. SSR applications should use createSessionRefresher for in-process single-flight rotation.

Handle other entry points

  • requestMagicLink and consumeMagicLink implement passwordless login.
  • forgotPassword always returns success when accepted, whether or not the email exists.
  • resetPassword consumes a single-use token and invalidates existing sessions.
  • getOAuthAuthorizationUrl returns a provider URL; the app performs navigation.
  • exchangeOAuth consumes the callback token and stores the returned session.

Validate every returnTo path before redirecting after login or OAuth.

Verify the journey

  1. Register a new address and render the unverified state.
  2. Verify by both supported UI paths if you expose both.
  3. Reload and confirm the selected storage mode behaves as intended.
  4. Expire the access token and confirm one refresh restores the private read.
  5. Revoke the refresh token and confirm the app returns to signed-out state.
  6. Log out and verify board.me.retrieve() no longer succeeds.

Production notes

  • SSR apps must keep tokens in an HttpOnly cookie.
  • Configure that session cookie with HttpOnly, Secure, and an appropriate SameSite policy. Protect state-changing cookie-authenticated requests against CSRF; cookie flags do not replace CSRF protection.
  • Give email, current-password, and new-password controls visible labels and correct autocomplete values so password managers and assistive technology can identify them.
  • Preserve the intended return path without accepting open redirects.
  • Display validation details near the corresponding field and log the API request ID for unexpected failures.
  • If logout fails before server revocation, the SDK retains local tokens so the application can retry. Decide separately whether to offer a force-local-logout action.

See the Auth reference for every method and response type.