Back to blog

PHP Earns the Money. Next.js Shows It.

In a headless Bitrix + Next.js architecture, the split is straightforward in theory: Bitrix owns the data and business logic, Next.js owns the rendering. In practice, most teams get this wrong within the first year.

When a client says "we want to go headless," the first thing I ask is: which part of the system are we actually talking about?

Most of the time they mean: "we want a nice frontend in Next.js." Nobody means: "we want to figure out who owns which failure mode." That's a mistake. The second question is what determines whether the architecture stays maintainable or quietly becomes two poorly coupled systems by the time someone else inherits it.

I've been running headless projects on Bitrix and Next.js for a few years. One of them is 28,000 SKUs — industrial equipment, B2B pricing with tax, three warehouses, dealer discount tiers. Here's how I think about the layer split — and where we got it wrong.

What "earning" means

In headless Bitrix + Next.js, PHP and Bitrix own everything with direct business consequences: catalog data, pricing rules, inventory, order processing, and CRM. Every iblock record is either direct revenue (product, price, stock status) or a precondition for it (discount rule, order state, warehouse assignment).

Everything that has money consequences lives in Bitrix:

  • catalog: 28,000 SKUs, PROPERTY_* fields, group pricing, units of measure
  • pricing: base prices, dealer markups, promo rules
  • warehouse logic: stock across three locations, reservations, expected restock dates
  • orders: cart, payment, 1C integration, shipping statuses
  • CRM: leads, deals, customer interaction history

All of that is years of business decisions encoded as data structure. "Rewriting" it doesn't mean migrating — it means recreating every rule from scratch. That's not a frontend job.

PHP stays. It earns.

What "showing" means

Next.js in this architecture doesn't know what the price is. It makes an HTTP request to Bitrix REST API, gets back an object with an already-calculated price, and renders the number. That's the whole relationship.

Next.js covers the other side of the contract:

  • routing and SEO markup for catalog pages, product cards, categories
  • rendering: SSR, ISR (static with incremental regeneration), client-side for search
  • UI components: filters, carousels, cart widget (but not cart logic)
  • CDN delivery: static assets, edge caching of response headers
  • performance: Core Web Vitals, LCP under 2.5s, minimal client-side JavaScript

Next.js doesn't compute who gets which discount. It doesn't know if the product is in stock: it receives an in_stock: true/false flag from the API and displays "available" or "on order." That's not a limitation. That's the right separation.

So Next.js shows. Only shows.

Three cases where the split saves you

This sounds abstract until you're working through real incidents.

Bitrix goes into maintenance

Bitrix needs a security update. A small site puts up a maintenance window and closes for an hour. That doesn't work for us — B2B, industrial equipment, orders come in during business hours.

Solution: Next.js on ISR. Catalog and product pages are generated and cached. While Bitrix is down, Next.js serves stale versions. Users browse, add to cart — the cart state persists in localStorage. When Bitrix comes back up, the cart syncs.

Bitrix is down for 20 minutes. The site works. Sales happen.

Without headless that wouldn't be possible. Bitrix would own both data and rendering. When it goes down, everything goes down.

The Next.js deploy breaks

We shipped a new version of the filter component. Tested fine in staging. In production, a white screen for a subset of users. Not fun.

But: checkout was routed through a direct URL pointing at the Bitrix backend. Not through Next.js. While we were rolling out the hotfix, B2B clients were placing orders through the direct URL — account managers shared it in chat.

Next.js was broken for two hours. Revenue didn't drop.

Frontend A/B test with no backend risk

We need to test a hypothesis: product card with video converts better than without. Next.js spins up two component variants, traffic splits 50/50. Bitrix data stays the same, REST API stays the same, no backend code changes.

The test takes a day to set up. Rolls back in an hour. Bitrix never knew we were testing anything.

Where the split breaks

The most common failure: business logic starts drifting into Next.js "for convenience" or "for performance."

This happened to us with discount calculation. The logic seemed obvious: the promo price comes from Bitrix as price_sale, so Next.js computes the discount percentage itself (price_sale / price_base * 100). Simple math, right? Why make an extra API call for one number?

Three months later, a Bitrix admin changed the price rounding rule. Base prices started rounding differently. Bitrix recalculated everything correctly. Next.js kept using the old formula. For three months, part of the product cards showed a wrong discount percentage. Customers saw "-23%" when the actual discount was -19%.

Rolling it back took a day. The lesson stuck: anything with money consequences gets calculated in Bitrix. Next.js renders the result.

The second failure mode: a content editor starts storing data in Next.js components (hardcoded prices in JSON files in the repo, for instance). Now a Next.js deploy is a business data deploy. That's dangerous in a different way.

What this looks like in practice

A working headless Bitrix + Next.js contract has strict boundaries between layers. For 28k SKUs, ours ended up at 12 REST endpoints on the Bitrix side. Strict types. No business logic in the frontend layer. Every endpoint returns pre-calculated data: prices with group discounts already applied, stock based on priority warehouse, availability flags.

Next.js deploys via PM2 in 3 minutes, independent of Bitrix. Bitrix deploys in 40 minutes over FTP, requires staging validation. Different cadences, different owners. They don't block each other.

Monitoring is separate: health check for Next.js (/api/healthz), health check for Bitrix REST API (/rest/api/health). Different Telegram alerts, different on-call. Boring setup. Works every time.

One line for every decision

When you're not sure which layer owns a feature:

If the feature reads or writes anything with money consequences — that's Bitrix. If it only renders — that's Next.js.

Price — Bitrix. Stock level — Bitrix. Order status — Bitrix. Homepage banner — Next.js. Add-to-cart animation — Next.js. Formatting 2499 as "$24.99" — Next.js.

If the feature sits on the boundary (cart with local state and sync, for example), it needs an explicit protocol: who wins on conflict. On data, Bitrix is the source of truth. On display, Next.js is.

Following this is inconvenient in the short term. A year later, it's what holds the system together.


More on how deploy independence works in practice: headless-deploy-independence. On the REST API contract for 28k SKUs: bitrix-rest-api-headless-surprises. On cache invalidation when Bitrix updates a product: nextjs-isr-bitrix-cache-invalidation.