Skip to content
pcreative Commerce

How the panel talks to the backend

The management panel is a separate application.

The management panel is a separate application. It does not share a process with the backend, does not share a database and does not import its code: it speaks to it over HTTP, like any other program would.

This page explains why it is built this way, which is the part you cannot work out from reading the code.

The token is not in the browser

This is the decision everything else hangs from.

The usual thing in an admin panel is to keep the session token in localStorage and send it as Authorization: Bearer …. It works, it is convenient, and it has a problem you do not see until it is late: any execution of somebody else's JavaScript in the panel walks off with it. A compromised dependency, a text field rendered without escaping, a browser extension. And with that token you get in from anywhere, with no password and without leaving a single failed attempt in a log anyone could look at.

Here the token lives on the panel's own server. The browser only holds a cookie:

HttpOnly · Secure · SameSite=Strict · __Host- prefix

HttpOnly means the page's JavaScript cannot read it. Stealing the token stops being possible because there is nothing in the browser to steal.

HttpOnlythe page's script cannot see it
Secureit never travels outside HTTPS
SameSite=Strictit is not sent on requests coming from another site, which is what stops CSRF
__Host-the browser ties it to this exact domain and to /; a subdomain cannot overwrite it

⚠️ __Host- requires Secure, and the browser silently rejects a __Host- cookie without it. No error, no warning: signing in looks fine and the session is never stored — you log in, and you are back at the login screen. That is why the prefix is only applied when the connection allows it (localhost, or real HTTPS as seen through X-Forwarded-Proto). If your proxy does not send that header, there will be no prefix, and it is worth knowing before you lose an afternoon.

Calls go through the panel itself

Screens do not call the backend: they call /admin/api/backend/*, which is a route of the panel. There, on the server, the cookie is read, the token is taken out and the request is forwarded.

That has three consequences worth more than the convenience it costs:

  1. The panel's screens need no CORS. The browser talks to its own origin. CORS_PANEL still exists in the backend, for a browser on another origin calling /gestion directly; Docker fills it from PANEL_URL.
  2. The backend is not exposed to the browser. Only the panel speaks to it.
  3. The backends are on a server-side allowlist. The proxy does not forward wherever it is told: it only speaks to the backends the panel knows — PCC_BACKEND_URL and the backendUrl of each shop in STORES— and anything else falls back to the first one. A tampered parameter does not turn the panel into a bridge to another machine.

The seller panel (apps/vendedores, port 7002) follows the same pattern: the token lives on its server behind an HttpOnly cookie, and screens call its own /api/backend/*. There the list is of routes: it only forwards the paths the seller panel needs, and answers 403 to anything else. It only speaks to one backend, PCC_BACKEND_URL.

The client is ours, and why

@pcreative/admin-client. No dependencies.

There used to be a third-party SDK, and it was replaced after looking at actual usage: of the panel's calls, well over half were a plain HTTP request and the rest spread across some thirty methods. In exchange for that thin wrapper, dozens of the panel's files were tied to an external dependency and to its way of naming things.

What did have to be replicated exactly is the shape of the request, because that is not a design decision: it is what the backend expects on the other side.

  • The query string is serialised with brackets: filter[field]=value, and lists are indexed, ids[0]=a&ids[1]=b.
  • Nulls are omitted.
  • Cookies travel.
  • The error carries the backend's message, not the generic one for the status code.

Getting any of those four wrong produces no error at all: a filter that matches nothing is sent, the backend answers 200 with an empty list, and the screen says "no products" over a full catalogue. That is why the client has tests about the format and not about whether it "works".

One panel, several shops

The shops the panel can manage are declared in STORES (a JSON with, for each shop, its backendUrl, storefrontUrl, brand and tax details). With more than one, a shop selector appears on the login screen; the choice is saved in the pcc-tienda cookie and, on reload, the panel applies that shop's configuration. Activating a theme does not change the backend: the theme is what the storefront shows, not where the panel speaks.

The session belongs to each backend, so changing shop means signing in again. That is not an oversight: a session that worked across several shops would turn the panel into a master key.

To deploy it

Behind a reverse proxy, the one thing you cannot forget is X-Forwarded-Proto, for the reason above. The ports and a Caddyfile for the whole shop are in Installation.