Skip to main content

Documentation Index

Fetch the complete documentation index at: https://setup.despia.com/llms.txt

Use this file to discover all available pages before exploring further.

A native session bridge into your page’s server middleware. The bridge writes the session into the WebView’s cookie store (and optionally injects a request header) so Next.js, TanStack, or any framework’s middleware sees an authenticated user on the very first request, no Clerk web SDK on the page required. On by default with sensible scoping, most apps never need to tune it.

Parameters

ParamDefaultNotes
enabledtruefalse purges every SSR cookie
headernoneHeader name to inject on main-frame navigations. Authorization gets a Bearer prefix, any other name gets the raw JWT
domainsapp host + localhost + 127.0.0.1Comma-separated override of the cookie/host scope
// Default, already on
despia('clerk://ssr')

// Inject Authorization on main-frame nav
despia('clerk://ssr?header=Authorization')

// Custom domain scope (separate API host)
despia('clerk://ssr?domains=app.example.com,api.example.com')

// Opt out and purge
despia('clerk://ssr?enabled=false')
Emits:
{
    "ok": true,
    "event": "ssr",
    "status": "enabled",
    "domains": ["app.example.com", "localhost", "127.0.0.1"],
    "header": "X-Clerk-JWT"
}

Cookies

Three cookies, scoped per configured domain, refreshed every 50 seconds.
CookieValueUsed by
__sessionSession JWTClerk’s clerkMiddleware()
__client_uatUnix-seconds signed in, 0 signed outClerk’s middleware signed-in/out signal
clerk_tokenSession JWTManual @clerk/backend verification or client-side reads
Cookies are not HttpOnly (so client code on localhost can read clerk_token), Secure on real hosts, and not Secure on localhost / 127.0.0.1 so they work over http://localhost:port.

Header transport

With header=, the bridge intercepts main-frame GETs to configured domains and attaches the header.
despia('clerk://ssr?header=Authorization')
// Every page load now arrives with: Authorization: Bearer <jwt>
Authorization gets a Bearer prefix. Any other header (X-Clerk-JWT, X-Auth) gets the raw JWT.

Next.js

clerkMiddleware() reads __session and __client_uat from request cookies, so the standard middleware just works.
// middleware.ts
import { clerkMiddleware } from '@clerk/nextjs/server'
export default clerkMiddleware()
// app/dashboard/page.tsx
import { auth } from '@clerk/nextjs/server'

export default async function Page() {
    const { userId } = await auth()
    return <Dashboard userId={userId} />
}
No <ClerkProvider>, no useAuth() hook, no web SDK. The native bridge writes the cookies and the middleware does the rest.

Manual verification

For any other backend, read the JWT and verify it with @clerk/backend.
import { verifyToken } from '@clerk/backend'

const jwt = req.cookies['clerk_token']  // or req.headers['x-clerk-jwt']

const { sub: userId } = await verifyToken(jwt, {
    secretKey: process.env.CLERK_SECRET_KEY,
})
Trust only the verified sub. Never accept a client-reported userId.

Handshake invariant

Clerk’s middleware treats “a live __client_uat next to a missing/expired __session” as a redirect to Clerk’s FAPI for a handshake, which a native WebView with no Clerk web SDK cannot satisfy. The bridge guarantees the cookie pair is only ever in one of two states middleware accepts without a handshake:
  • Signed-in: fresh JWT in __session plus __client_uat > 0
  • Signed-out: no __session plus __client_uat = 0
Enforced structurally with a TTL ladder: the 50-second refresher runs under the 55-second cookie TTL, which runs under the 60-second JWT TTL. An un-refreshed cookie self-expires before its JWT does, so the browser can never present the handshake-triggering combination. Degradation is always toward signed-out, never toward broken handshake. A navigation gate adds a second layer. On a main-frame GET to a configured domain with a live session, if the cookie is stale (>45s) or the opt-in header is missing, the navigation is cancelled, a fresh JWT is minted, the cookie write is awaited, the header is attached, and the request is re-issued. Self-terminating, cannot loop.

Cold launch

The first page load of a cold launch is anonymous, Clerk is not configured until your page’s JS has run and called clerk://configure. That first load lands in the safe signed-out state, never a handshake. Every navigation after configure is authenticated. If you need authenticated SSR on the very first byte, configure earlier in your boot or render a brief loading shell that calls configure and then navigates to the real route once window.clerkJWT is populated.

Offline

A failed refresh leaves the existing cookies in place. Once they hit their 55-second TTL the browser presents the signed-out state to middleware rather than the handshake combination. The user effectively becomes signed-out for new server-rendered requests until the device comes back online.

Resources

NPM Package

despia-native