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- prefixHttpOnly means the page's JavaScript cannot read it. Stealing the token stops
being possible because there is nothing in the browser to steal.
What each part of that cookie does
HttpOnly | the page's script cannot see it |
Secure | it never travels outside HTTPS |
SameSite=Strict | it 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-requiresSecure, 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 throughX-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:
- The panel's screens need no CORS. The browser talks to its own origin.
CORS_PANELstill exists in the backend, for a browser on another origin calling/gestiondirectly; Docker fills it fromPANEL_URL. - The backend is not exposed to the browser. Only the panel speaks to it.
- 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_URLand thebackendUrlof each shop inSTORES— 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.