TypeScript client

Storage adapters

Choose browser, cookie, or custom persistence for client tokens and authorization state.

Storage adapters persist three client values:

KeyPurposeSensitivity
tokenCurrent access tokenBearer credential
verifierPKCE verifier for the pending flowTemporary secret
stateCallback correlation valueTemporary security value

Adapter contract

import type {
  StorageAdapter,
  StorageAdapterKey,
} from "unbound-auth";

interface StorageAdapter {
  get(key: StorageAdapterKey): Promise<string | null>;
  set(key: StorageAdapterKey, value: string): Promise<void>;
  remove(key: StorageAdapterKey): Promise<void>;
  query?(
    keys: StorageAdapterKey[],
  ): Promise<Partial<Record<StorageAdapterKey, string | null>>>;
  mutate?(
    values: Partial<Record<StorageAdapterKey, string | null>>,
  ): Promise<void>;
}

The client prefers optional batch methods when available. In mutate, null means delete. A custom adapter should preserve exact strings, reject failed operations, and make batch behavior equivalent to individual calls.

Browser storage

The default browser adapter splits persistent session data from tab-scoped authorization state:

KeyDefault locationDefault storage key
tokenlocalStorageunbound_token
verifiersessionStorageunbound_verifier
statesessionStorageunbound_state

Security requirement

A token in localStorage is readable by every script executing on the app origin. An XSS vulnerability can exfiltrate it. Use a strong content-security posture, minimize third-party script access, and consider a server session with HttpOnly cookies when that better fits your threat model.

State and verifier survive navigation in the same tab but are not shared with another tab. The adapter holds one pending pair per prefix, so parallel sign-ins can collide.

Use a custom prefix when multiple independent clients share an origin:

import { createUnboundClient } from "unbound-auth";
import { browserStorageAdapter } from "unbound-auth/adapters";

const auth = createUnboundClient({
  redirect_uri: "/auth/callback",
  scopes: ["openid", "profile", "email"],
  storage: browserStorageAdapter({ prefix: "customer_portal_" }),
});

Browser storage can fail in restricted contexts or because of quota and security policies. Auth operations report these failures through their { data, error } result.

Custom adapter

A memory adapter can be useful for short-lived, non-persistent clients:

import type {
  StorageAdapter,
  StorageAdapterKey,
} from "unbound-auth";

export function memoryStorageAdapter(): StorageAdapter {
  const values = new Map<StorageAdapterKey, string>();

  return {
    async get(key) {
      return values.get(key) ?? null;
    },
    async set(key, value) {
      values.set(key, value);
    },
    async remove(key) {
      values.delete(key);
    },
  };
}

For server persistence, ensure an adapter is scoped to one request or one application session rather than shared mutable state.

Import the cookie adapter from unbound-auth/adapters. It supports standard web Request/Response objects and Node HTTP request/response objects.

Middleware mode

import { createUnboundClient } from "unbound-auth";
import { cookieStorageAdapter } from "unbound-auth/adapters";

const baseAuth = createUnboundClient({
  server: true,
  redirect_uri: "https://app.example.com/auth/callback",
  scopes: ["openid", "profile", "email"],
});

const authMiddleware = cookieStorageAdapter(baseAuth);

Call authMiddleware(req, res, next) using the middleware contract supported by your server. It assigns a cloned, request-scoped client to req.auth and initializes it lazily.

async function accountHandler(req: AccountRequest, res: AccountResponse) {
  const { data: session, error } = await req.auth.getSession();

  if (error) {
    res.sendStatus(401);
    return;
  }

  res.json(session?.user ?? null);
}

AccountRequest and AccountResponse above represent your framework's request and response types; they are not package exports.

Direct mode

When you already have a request and response, create the scoped client directly:

const requestAuth = cookieStorageAdapter(baseAuth, request, response);
const { data: session, error } = await requestAuth.getSession();

if (error) {
  handleSessionError(error);
} else {
  handleSession(session);
}

A response is required in direct mode because writes append Set-Cookie headers.

Security requirement

Keep baseAuth uninitialized and stateless. Never reuse req.auth, requestAuth, or any other cookie-backed request client across requests.

SettingDefault
Name prefixunbound_
HttpOnlyEnabled
SecureEnabled
SameSiteLax
Path/
DomainNot set
Token Max-AgeRemaining JWT lifetime, or 900 seconds without a usable exp
State/verifier lifetimeSession cookie

Default Secure cookies are not sent over ordinary local HTTP. Override secure only for deliberate local development, and keep secure transport in deployed environments. If you set SameSite=None, browsers also require Secure.

Cookie reads reflect the incoming request, not cookies newly written to the current response. Plan multi-step work accordingly.

Continue with the server flow or review local logout behavior.

On this page