The authorized user sees the login form. Bitrix is up. Next.js is up. They just can't agree on who you are.
We moved a 28,000-product catalog to Next.js. The first two days after deploy were about CSS and performance. On day three, a ticket arrived: "Authenticated users see the login form instead of their account."
Bitrix was up. Next.js was up. Logs weren't screaming. But from the frontend's perspective, every logged-in user was nobody.
This isn't a code bug. It's three separate session problems that surface when your frontend and backend live on different domains. Here's each one.
How Bitrix handles sessions in a monolith
In a classic stack it's simple. Bitrix stores the session in $_SESSION, the browser gets a PHPSESSID cookie with httpOnly; SameSite=Lax. Every subsequent request sends that cookie back — Bitrix reads it, retrieves the user from the session, everything works.
Bitrix also uses BX_SESSID — an anti-CSRF token. Every POST through the REST API needs an X-Bitrix-Csrf-Token header with this value. Without it: 403. This isn't documented clearly, but it's how it works.
In a monolith you never think about it. Everything's on one domain, the browser carries the cookie automatically, CSRF is handled behind the scenes.
What breaks when the domains are different
Frontend: shop.example.com. Backend (Bitrix API): api.example.com.
The SameSite cookie policy is a browser security standard that controls whether cookies are sent with cross-site requests. By default (SameSite=Lax), the browser won't send PHPSESSID to api.example.com when the request originates from shop.example.com. Cross-domain cookies need SameSite=None; Secure — which requires HTTPS on both domains and explicit permission in Bitrix's configuration.
We set up CORS on Bitrix (Access-Control-Allow-Origin, Access-Control-Allow-Credentials: true), set SameSite=None, and client-side fetch requests with credentials: "include" started working.
That fixed half the problem.
BX_SESSID: the silent 403 on every POST
The account page loaded. But every user action returned 403. Add to cart, save address, place order.
BX_SESSID is Bitrix's anti-CSRF token, generated at session initialization and required in the X-Bitrix-Csrf-Token header on every POST request through the REST API. Without it, Bitrix returns 403 silently. In a monolith it's injected automatically through a PHP template. In headless you need to fetch it explicitly: a separate GET request to Bitrix at session startup, then include it in every POST.
The flow: at login, grab BX_SESSID from the Bitrix response (field result.SESSID), store it in frontend state or cookie, send it in the header with every mutating request.
Sounds simple. But the documentation doesn't describe this. We figured it out from access.log: requests without the header returned 403 silently, with no explanation.
The third problem: SSR without client context
Server-side rendering (SSR) in Next.js executes API calls before sending HTML to the browser — via getServerSideProps or Server Components. This improves SEO and reduces first-load time.
But SSR calls Bitrix without the browser's cookie. The Next.js server doesn't know who's logged in. If you want to render personalized content server-side (cart, wishlist, customer-specific prices), you need extra plumbing.
Three options.
First — cookie forwarding. In getServerSideProps, read the cookie from the incoming request (req.cookies) and pass it in a Cookie header to Bitrix. Works but fragile: the user's cookie flows through server-side code, and you need to be careful about security when running SSR on Vercel or any CDN.
Second — JWT. On login, Bitrix issues a JWT token (via a custom endpoint or OAuth2). Next.js stores it in an httpOnly cookie; both server and client can use it as a Bearer token. Bitrix-side you need a custom JWT validation implementation. More work, but clean.
Third — hybrid: CSR for authenticated content. Personalized parts render client-side. Static content goes SSR: catalog, product pages, descriptions. The account dashboard renders on the client after hydration.
What we chose — and why
On the 28k SKU project we went with the hybrid approach: SSR for public content, CSR for anything user-specific.
Specifically:
- Catalog, product pages, categories — SSR, no auth.
- Cart, account, customer-specific prices — client-side rendering after hydration.
- SSR with auth: only if SEO and personalization overlap. In our case: zero pages.
BX_SESSID is fetched at session start via a GET, kept in client state, added to every mutating request header.
SameSite=None required editing Bitrix's security settings (.settings.php) and explicitly adding the frontend domain to the allowed CORS origins.
We dropped JWT: not enough time to build a custom endpoint, and Bitrix's built-in OAuth2 is designed for external apps, not your own frontend.
Checklist before you launch
If you're running headless Bitrix + Next.js with logged-in users, go through this before you go live:
- CORS on Bitrix is set with
credentials: trueand an explicitAllow-Origin(not*). - Bitrix cookies have
SameSite=None; Secure— and both domains have HTTPS. - On login, you capture
SESSIDfrom the Bitrix response and store it client-side. - POST requests through the REST API include
X-Bitrix-Csrf-Token: <BX_SESSID>. - SSR requests to Bitrix are either unauthenticated, or pass cookies from
req.cookiesexplicitly. - Test your account page in incognito — cleanest signal without browser cache.
- Check DevTools → Network: confirm the cookie actually lands on the Bitrix domain (SameSite
Include).
Three days to figure this out is the honest price for something nobody wrote down.
Now my pre-project checklist has a new question: "Do authenticated users touch the catalog, or is auth only at checkout?" If yes — I add three days to the estimate and explain why.
*More on the API side of headless Bitrix: Five things Bitrix REST API doesn't tell you before you go headless. On what to keep on Bitrix vs what to move to Next.js: What we kept on Bitrix when going headless.*