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:
- An OAuth client (set up automatically on deploy, or via the desk dashboard for custom apps)
- Two env vars:
CIMPLIFY_CLIENT_ID,CIMPLIFY_REDIRECT_URI - Three route files:
/auth/callback,/auth/session,/auth/signout - The sign-in button somewhere in your UI
bun add @cimplify/sdkAuth Surfaces
| Surface | Host | Used for | How the shopper verifies |
|---|---|---|---|
| Storefront OAuth | auth.cimplify.io | Storefront sign-in and embedded checkout | Hosted passkey/OTP/consent, then callback to the merchant app |
| Direct Link API | api.cimplify.io/v1/link/auth/* | Custom Link account surfaces and Cimplify-hosted customer session restore | Link 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
"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
"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
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.
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!,
});
}import { handleSessionRequest } from "@cimplify/sdk/server";
export async function GET(req: Request): Promise<Response> {
return handleSessionRequest(req, {
clientId: process.env.CIMPLIFY_CLIENT_ID!,
});
}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:
| Cookie | Holds | TTL | Purpose |
|---|---|---|---|
cimplify_session | The verified ID token (JWT) | 1 hour | Identity claims (sub, name, email, phone). Read in Server Components via getSessionFromCookieHeader. |
cimplify_access | The OAuth access token (JWT) | 1 hour | Authenticates 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:
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:
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 = 0on the route, which disables ISR entirely.next: { revalidate: 0 }viacacheOptions: { revalidate: 0 }on individual reads inside an otherwise-cacheable page.- Render the personalized block inside a
<Suspense>boundary streamed by a Server Component that usesgetAuthenticatedServerClient(), 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.
"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:
| Option | Purpose |
|---|---|
loginHint | Pass 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. |
silentFirst | In modal mode, try hidden prompt=none before opening visible UI. |
silentTimeoutMs | Override 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 deploycreates one for each project and injectsCIMPLIFY_CLIENT_ID/CIMPLIFY_REDIRECT_URIinto 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
| Export | From | Purpose |
|---|---|---|
<CimplifySignInButton> | @cimplify/sdk/react | The drop-in button |
useCimplifySession() | @cimplify/sdk/react | Reactive session hook |
startSignIn(opts) | @cimplify/sdk | Start sign-in from anywhere (redirect by default; mode: "modal" for the overlay) |
signInSilent(opts) | @cimplify/sdk | Try cross-storefront SSO without UI (needs third-party cookies) |
handleRedirectCallback(req, cfg) | @cimplify/sdk/server | GET /auth/callback. Completes the redirect flow: exchanges the code, sets both cookies, 302s to returnTo. |
handleOidcCallback(req, cfg) | @cimplify/sdk/server | POST /auth/callback. Modal/silent exchange. Sets both identity + access cookies. |
handleSessionRequest(req, cfg) | @cimplify/sdk/server | GET /auth/session |
getSessionFromCookieHeader(cfg, header) | @cimplify/sdk/server | Read identity claims in a Server Component |
getAccessTokenFromCookieHeader(cfg, header) | @cimplify/sdk/server | Read the OAuth access token (server only) |
buildSessionCookie(cfg, token, ttl) | @cimplify/sdk/server | Manually build the identity cookie |
buildAccessTokenCookie(cfg, token, ttl) | @cimplify/sdk/server | Manually build the access cookie |
buildSignoutCookies(cfg) | @cimplify/sdk/server | Returns both clear-cookie strings for /auth/signout |
buildSignoutCookie(cfg) | @cimplify/sdk/server | Legacy; clears only the identity cookie. Use buildSignoutCookies. |
Related
- Orders: fetch orders for the signed-in shopper using
session.sub - Checkout: start checkout against the authenticated session
- Server Components:
getServerClient, cache tags, revalidation
Checkout
Convert a cart into a paid order. The body is **flat**: fields like ` cart_id`, `customer`, `order_type`, and `payment_method` sit at the top level (not nested under any envelope). Production uses ` #[serde(flatten)]`; the SDK matches that shape exactly.
Orders
List, retrieve, and cancel orders. Anonymous orders carry a short-lived `bill_token` that the SDK persists for you, so guest order lookups Just Work after checkout.