cimplify
TypeScript SDK

Sign in with Cimplify

Add Cimplify sign-in to your storefront. One button, three route handlers, one React hook. Shoppers sign in via passkey or one-time code, then land back on your page signed in.

If you scaffolded your storefront with cimplify init, sign-in is already wired. Skip ahead to The button and drop it on your page. The route files and env vars are pre-installed.

For a custom integration, you need four things:

  1. An OAuth client (set up automatically on deploy, or via the desk dashboard for custom apps)
  2. Two env vars: CIMPLIFY_CLIENT_ID, CIMPLIFY_REDIRECT_URI
  3. Three route files: /auth/callback, /auth/session, /auth/signout
  4. The sign-in button somewhere in your UI
bun add @cimplify/sdk

Auth Surfaces

SurfaceHostUsed forHow the shopper verifies
Storefront OAuthauth.cimplify.ioStorefront sign-in and embedded checkoutHosted passkey/OTP/consent, then callback to the merchant app
Direct Link APIapi.cimplify.io/v1/link/auth/*Custom Link account surfaces and Cimplify-hosted customer session restoreLink OTP endpoints issue a bearer session_token and cim_session refresh cookie

Agent rule of thumb: storefront sign-in and checkout use OAuth. Customer account pages in a merchant storefront use the signed-in storefront session plus SDK components; direct client.link.* integrations use the Link API session token.

The button

components/sign-in.tsx
"use client";

import { CimplifySignInButton } from "@cimplify/sdk/react";

export function SignIn() {
  return (
    <CimplifySignInButton
      clientId={process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID!}
      redirectUri={`${window.location.origin}/auth/callback`}
    />
  );
}

That's it. Clicking it takes the shopper to Cimplify's hosted sign-in (passkey or one-time code), then returns them to the page they came from, signed in. You render no forms and store no passwords.

Variants: primary (default green), outline, dark, text.

<CimplifySignInButton variant="dark" label="Sign in" fullWidth />

Redirect vs. modal

Sign-in defaults to a redirect: a top-level navigation to Cimplify and straight back to where the shopper started. It works in every browser and on any domain (including your custom domain) because Cimplify is first-party during the hop. Returning shoppers who already have a Cimplify session bounce through in a flash, no UI.

An in-page modal is available as an opt-in:

<CimplifySignInButton mode="modal" clientId={} redirectUri={} />

The modal renders an in-page sign-in overlay, so the shopper never navigates away. But it depends on third-party cookies, which Safari and Firefox block (and which don't work on custom domains at all). Use it only when you know your traffic is on a browser that allows them; otherwise stay on the default redirect.

Either way, checkout and the rest of your storefront stay in-page. The hand-off is only for the sign-in itself.

Read the session in React

components/header.tsx
"use client";

import { CimplifySignInButton, useCimplifySession } from "@cimplify/sdk/react";

export function Header() {
  const { session, loading } = useCimplifySession();
  if (loading) return null;
  return session ? (
    <span>Hi, {session.name}</span>
  ) : (
    <CimplifySignInButton
      clientId={process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID!}
      redirectUri={`${window.location.origin}/auth/callback`}
    />
  );
}

The session shape:

{
  sub: string;              // your customer id: stable, use as DB key
  name?: string;
  email?: string;
  emailVerified?: boolean;
  phoneNumber?: string;
  phoneNumberVerified?: boolean;
}

sub is your customer's stable identifier. Store it in your database as the foreign key for orders, carts, anything you persist.

Read the session on the server

app/page.tsx
import { cookies } from "next/headers";
import { getSessionFromCookieHeader } from "@cimplify/sdk/server";

export default async function Page() {
  const session = await getSessionFromCookieHeader(
    { clientId: process.env.CIMPLIFY_CLIENT_ID! },
    (await cookies()).toString(),
  );
  if (!session) return <SignInPrompt />;
  return <Greeting name={session.name} sub={session.sub} />;
}

Route handlers

Three files, copy-paste. If you ran cimplify init, these are already in your repo.

app/auth/callback/route.ts
import { handleOidcCallback, handleRedirectCallback } from "@cimplify/sdk/server";

// Redirect sign-in lands here via a top-level GET carrying ?code. The
// handler exchanges the code, sets the cookies, and 302s back to the
// page the shopper started on.
export async function GET(req: Request): Promise<Response> {
  return handleRedirectCallback(req, {
    clientId: process.env.CIMPLIFY_CLIENT_ID!,
    redirectUri: process.env.CIMPLIFY_REDIRECT_URI!,
    defaultReturnTo: "/account",
  });
}

// The modal/silent flows POST {code, codeVerifier, state} here instead.
export async function POST(req: Request): Promise<Response> {
  return handleOidcCallback(req, {
    clientId: process.env.CIMPLIFY_CLIENT_ID!,
    redirectUri: process.env.CIMPLIFY_REDIRECT_URI!,
  });
}
app/auth/session/route.ts
import { handleSessionRequest } from "@cimplify/sdk/server";

export async function GET(req: Request): Promise<Response> {
  return handleSessionRequest(req, {
    clientId: process.env.CIMPLIFY_CLIENT_ID!,
  });
}
app/auth/signout/route.ts
import { buildSignoutCookies } from "@cimplify/sdk/server";

export async function POST(): Promise<Response> {
  const headers = new Headers({ "Content-Type": "application/json" });
  for (const cookie of buildSignoutCookies({ clientId: process.env.CIMPLIFY_CLIENT_ID! })) {
    headers.append("Set-Cookie", cookie);
  }
  return new Response(JSON.stringify({ ok: true }), { status: 200, headers });
}

buildSignoutCookies (plural) returns two Set-Cookie strings: one for the identity cookie and one for the access-token cookie (see Two cookies, one identity). buildSignoutCookie (singular) only clears the identity cookie; new integrations should use the plural form.

Two cookies, one identity

Sign-in sets two HttpOnly cookies on your storefront's origin:

CookieHoldsTTLPurpose
cimplify_sessionThe verified ID token (JWT)1 hourIdentity claims (sub, name, email, phone). Read in Server Components via getSessionFromCookieHeader.
cimplify_accessThe OAuth access token (JWT)1 hourAuthenticates API calls so Cimplify can return per-customer data (their orders, their assigned price list, their subscriptions). Read on the server only; never expose to the browser.

Both are HttpOnly, Secure, SameSite=Lax. Both are cleared by buildSignoutCookies and the /auth/signout route. The browser never sees the access token; you pass it from server → API via the authenticated server client (next section).

When the tokens expire, the next sign-in re-issues them. The shopper's global session on .cimplify.io (90-day refresh token) survives, so a returning shopper is recognized immediately; the redirect bounces through with no UI.

Personalized server-side reads

A bare getServerClient() makes API calls with the storefront's public key only. Cimplify returns the guest view: public catalogue, no orders, no per-customer pricing. To get personalized data, attach the customer's access token.

If you scaffolded with cimplify init, the helper is already in lib/auth.ts. Just import and use:

app/account/orders/page.tsx
import { getAuthenticatedServerClient } from "@/lib/auth";

export const revalidate = 0; // personalized; do not ISR-cache

export default async function OrdersPage() {
  const client = await getAuthenticatedServerClient();
  const r = await client.orders.list({ limit: 20 });
  if (!r.ok) throw new Error(r.error.message);

  return (
    <ul>
      {r.value.map((o) => (
        <li key={o.id}>
          #{o.order_number} · {o.status} · {o.total_price}
        </li>
      ))}
    </ul>
  );
}

This returns this customer's orders at this merchant, scoped via the pseudonymous sub in the ID token. Categories, products, and pricing returned by client.catalogue.* also reflect any price list assigned to the customer.

Custom integrations (no template)

If you didn't scaffold with cimplify init, drop this helper into your project. It mirrors what the template ships:

lib/auth.ts
import { headers } from "next/headers";
import {
  getAccessTokenFromCookieHeader,
  getServerClient,
  getSessionFromCookieHeader,
  type CimplifyClient,
  type CimplifySession,
} from "@cimplify/sdk/server";

const CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";
// Optional: override the OIDC issuer (e.g. for local dev). Defaults to
// https://api.cimplify.io, from which every endpoint is discovered.
const ISSUER = process.env.CIMPLIFY_ISSUER;
const oidcConfig = { clientId: CLIENT_ID, issuer: ISSUER };

export async function getSession(): Promise<CimplifySession | null> {
  if (!CLIENT_ID) return null;
  const cookieHeader = (await headers()).get("cookie");
  return getSessionFromCookieHeader(oidcConfig, cookieHeader);
}

export async function getAuthenticatedServerClient(): Promise<CimplifyClient> {
  const cookieHeader = (await headers()).get("cookie");
  const accessToken =
    CLIENT_ID && cookieHeader
      ? getAccessTokenFromCookieHeader(oidcConfig, cookieHeader) ?? undefined
      : undefined;
  return getServerClient({ accessToken });
}

When accessToken is undefined (no cookie, expired, signed out), the client falls back to guest mode. Pages calling getAuthenticatedServerClient() get personalized data when the customer is signed in and the guest view otherwise; branch on getSession() first if you need different rendering for the two states.

Caching personalized pages

Personalized responses must not be ISR-cached across users. Use one of:

  • export const revalidate = 0 on the route, which disables ISR entirely.
  • next: { revalidate: 0 } via cacheOptions: { revalidate: 0 } on individual reads inside an otherwise-cacheable page.
  • Render the personalized block inside a <Suspense> boundary streamed by a Server Component that uses getAuthenticatedServerClient(), while the surrounding page stays cacheable with the guest client.

Don't share an ISR entry between guests and signed-in users; the first request wins and everyone sees the same data.

Cross-storefront auto sign-in

Shoppers who signed into any other Cimplify storefront recently can be signed in to yours silently. Call this on app boot. If it works, their session lands without any UI; if not, no harm done, just show the sign-in button.

components/silent-bootstrap.ts
"use client";

import { signInSilent } from "@cimplify/sdk";

export async function tryAutoSignIn() {
  const result = await signInSilent({
    clientId: process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID!,
    redirectUri: `${window.location.origin}/auth/callback`,
  });
  if (result.ok) window.location.reload();
}

Use it once, on mount, in your root layout. Pair with useCimplifySession so the UI updates the moment the silent flow finishes. It only fires where third-party cookies are allowed (it's best-effort: when it can't run, the shopper just signs in via the button's redirect).

Programmatic sign-in

To start sign-in from outside a button (e.g., a CTA inside checkout, a keyboard shortcut), call startSignIn directly. It defaults to the redirect flow; pass returnTo to control where the shopper lands after:

import { startSignIn } from "@cimplify/sdk";

await startSignIn({
  clientId: process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID!,
  redirectUri: `${window.location.origin}/auth/callback`,
  returnTo: "/checkout",
});

For the in-page overlay instead, pass mode: "modal" and use onSuccess (which only fires in modal mode; the redirect navigates away before any callback could run):

await startSignIn({
  clientId: process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID!,
  redirectUri: `${window.location.origin}/auth/callback`,
  mode: "modal",
  onSuccess: () => router.push("/checkout"),
});

Programmatic options:

OptionPurpose
loginHintPass the email or phone the shopper already typed. It becomes OAuth login_hint.
prompt"none" for silent, "login" for account switch, "consent" for a fresh consent prompt.
uiMode"checkout" tells hosted auth to use checkout-oriented copy and layout.
silentFirstIn modal mode, try hidden prompt=none before opening visible UI.
silentTimeoutMsOverride the silent attempt timeout; default is 900ms for modal silent-first.

Embedded checkout calls this for you with mode: "modal", uiMode: "checkout", loginHint, and silentFirst: true. If silent auth returns login_required or consent_required, the SDK opens the hosted modal for verification.

The hosted modal posts this message shape to the parent:

{
  type: "authorization_response",
  response: {
    code?: string;
    state?: string | null;
    iss?: string;
    error?: "login_required" | "consent_required" | string;
    error_description?: string;
  };
}

Sign out

POST to your /auth/signout route to clear the storefront cookie:

"use client";

export async function signOut() {
  await fetch("/auth/signout", { method: "POST" });
  window.location.reload();
}

The shopper stays signed in to other Cimplify storefronts. To sign them out everywhere, send them to https://link.cimplify.io/signout.

What you don't have to think about

  • OAuth client setup. cimplify deploy creates one for each project and injects CIMPLIFY_CLIENT_ID / CIMPLIFY_REDIRECT_URI into the build env.
  • Token verification. The SDK verifies every ID token before setting your cookie. You never see a bad token.
  • Consent. Cimplify hosts the consent screen. Shoppers grant share-name, share-email, etc. per merchant; you only see the fields they agreed to share.
  • Passkeys. When the shopper's device supports them, Cimplify's sign-in promotes the passkey path automatically. OTP-by-phone-or-email is the fallback.

API reference

ExportFromPurpose
<CimplifySignInButton>@cimplify/sdk/reactThe drop-in button
useCimplifySession()@cimplify/sdk/reactReactive session hook
startSignIn(opts)@cimplify/sdkStart sign-in from anywhere (redirect by default; mode: "modal" for the overlay)
signInSilent(opts)@cimplify/sdkTry cross-storefront SSO without UI (needs third-party cookies)
handleRedirectCallback(req, cfg)@cimplify/sdk/serverGET /auth/callback. Completes the redirect flow: exchanges the code, sets both cookies, 302s to returnTo.
handleOidcCallback(req, cfg)@cimplify/sdk/serverPOST /auth/callback. Modal/silent exchange. Sets both identity + access cookies.
handleSessionRequest(req, cfg)@cimplify/sdk/serverGET /auth/session
getSessionFromCookieHeader(cfg, header)@cimplify/sdk/serverRead identity claims in a Server Component
getAccessTokenFromCookieHeader(cfg, header)@cimplify/sdk/serverRead the OAuth access token (server only)
buildSessionCookie(cfg, token, ttl)@cimplify/sdk/serverManually build the identity cookie
buildAccessTokenCookie(cfg, token, ttl)@cimplify/sdk/serverManually build the access cookie
buildSignoutCookies(cfg)@cimplify/sdk/serverReturns both clear-cookie strings for /auth/signout
buildSignoutCookie(cfg)@cimplify/sdk/serverLegacy; clears only the identity cookie. Use buildSignoutCookies.
  • Orders: fetch orders for the signed-in shopper using session.sub
  • Checkout: start checkout against the authenticated session
  • Server Components: getServerClient, cache tags, revalidation

On this page