> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://www.openfort.io/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

# Cookie sessions

By default, the SDK keeps the user's session token in browser storage and sends it as a bearer token. Any script running on your page can read that token. Cookie sessions replace it with an **HttpOnly cookie on your own domain**: JavaScript can't read it, the browser attaches it to requests automatically, and your server can gate server-rendered routes on it.

The cookie is first-party because Openfort serves authentication from a hostname **under your domain** that you delegate with a CNAME. Nothing changes for native apps or for clients that keep using bearer tokens: both transports work on the same project at the same time.

:::info
Cookie sessions are for web apps served on a domain you control. Requests from `localhost` or a hosting provider's preview domain can't receive a cookie for your domain, so keep a bearer setup for local development and previews.
:::

## How it works

You choose two names:

| Name | Example | Purpose |
|------|---------|---------|
| Root domain | `example.com` | The cookie is scoped to this domain and all its subdomains (`Domain=.example.com`). It must be a registrable domain, not a subdomain and not a shared hosting domain such as `vercel.app`. |
| Delegated host | `openfort-auth.example.com` | A subdomain of the root domain that you point at Openfort with a CNAME. All SDK traffic goes through it. |

Your app can run on the root domain or on any subdomain of it (`app.example.com`), as long as that origin is in the publishable key's [allowed domains](https://www.openfort.io/docs/configuration/allowed-domains).

The SDK talks to the delegated host only: `https://openfort-auth.example.com/api` for the API, `/iframe` for the embedded wallet, and `/shield` for recovery. A sign-in response sets `openfort.session_token` with `Domain=.example.com; HttpOnly; Secure; SameSite=Lax` and returns no token in the body.

One root domain belongs to exactly one project environment. Two projects, or the test and live environments of one project, can't share a root domain.

## Enable cookie sessions

::::steps
### Add the domain

In the [dashboard](https://dashboard.openfort.io), select the environment (test or live) that your publishable key belongs to, then go to **Configuration** > **Security** > **Cookie sessions**. Enter the root domain and the delegated host, and click **Add domain**.

The dashboard shows the CNAME target, `customers.openfort.io`.

### Add one CNAME record at your DNS provider

```txt
openfort-auth.example.com.  CNAME  customers.openfort.io.
```

:::warning[Don't proxy the record]
If your DNS is hosted on Cloudflare, set the record to **DNS only** (grey cloud), not **Proxied** (orange cloud). Openfort terminates TLS for the delegated host on its own Cloudflare zone; proxying the record on your zone as well breaks routing, and the domain never becomes active.
:::

No TXT record or other verification step is needed. Openfort validates ownership over the CNAME and issues the certificate automatically.

### Verify the domain

Back in **Cookie sessions**, click **Verify**. The status moves from `Pending` to `Active` once the CNAME resolves and the certificate is issued, usually within a few minutes of the DNS change. If it shows `Failed`, check that the record exists and isn't proxied, then verify again.

### Allow your app's origin

Under **Allowed Origins** for the publishable key, add every origin your app is served from, for example `https://app.example.com` (and `https://example.com` if the app also runs on the root domain). The API only reflects these origins on credentialed requests, so a missing origin shows up as a CORS error in the browser.

### Switch the session transport

Once the domain is `Active`, set **Session transport** to **HttpOnly cookie** and save. **SameSite** defaults to `Lax`, which lets the cookie accompany top-level navigations into your site (links, OAuth redirects). `Strict` sends it on same-site requests only.

Bearer tokens keep working for this project: native apps and any client that doesn't set `customAuthDomain` are unaffected.

### Configure the SDK

Pass the delegated host as `customAuthDomain`. The SDK then derives the API, iframe, and Shield URLs from it, sends credentialed requests, and stores no token.

```ts
import { Openfort } from "@openfort/openfort-js";

const openfort = new Openfort({
  baseConfiguration: {
    publishableKey: "YOUR_OPENFORT_PUBLISHABLE_KEY",
  },
  shieldConfiguration: {
    shieldPublishableKey: "YOUR_SHIELD_PUBLISHABLE_KEY",
  },
  overrides: {
    customAuthDomain: "openfort-auth.example.com",
  },
});
```
::::

## What changes in your app

* `getAccessToken()` returns `null`. There is no token to forward. `HttpOnly` only hides the cookie from JavaScript in the browser; the browser still sends it to any server under your root domain, which reads it from the `Cookie` request header (see [Verify the session on your server](#verify-the-session-on-your-server)).
* Social login redirects back to `https://openfort-auth.example.com/iam/v2/auth/callback/<provider>`. Register that URL with each OAuth provider you use, in addition to the `api.openfort.io` callback if the same project also serves bearer clients. The **Cookie sessions** section in the dashboard shows the exact value.
* The OAuth callback redirects to your app with `user_id` only, without `access_token`. If you handle the callback yourself, call `storeCredentials({ userId })` without a token.
* The embedded wallet iframe moves from `embed.openfort.io` to your delegated host. Its device share lives in the iframe's storage, which is per origin, so existing users of a project that switches to cookies recover their wallet once per device.

## Verify the session on your server

The browser sends the cookie to your backend only if that backend is served on the root domain or one of its subdomains (`app.example.com`, `api.example.com`). A backend on another domain never receives it. If your page calls a backend on a different subdomain, send the request with `credentials: 'include'`.

To check the session, forward the `Cookie` header to the session endpoint on your delegated host together with your publishable key:

```ts [Next.js API route]
export async function getSessionUser(req: NextApiRequest) {
  if (!req.headers.cookie) return null;

  const response = await fetch("https://openfort-auth.example.com/api/iam/v2/auth/get-session", {
    headers: {
      cookie: req.headers.cookie,
      "x-project-key": process.env.NEXT_PUBLIC_OPENFORT_PUBLISHABLE_KEY!,
    },
  });
  if (!response.ok) return null;

  const session = await response.json();
  return session?.user ?? null;
}
```

A `null` body means there is no valid session for that cookie.

## Switch back

Set **Session transport** to **Bearer token** and save; clients that set `customAuthDomain` stop working until they drop it, while bearer clients are unaffected. A domain can only be deleted while the transport is **Bearer token**, so existing cookie users aren't locked out by accident.
