Blog ·
White-label panels: what the customer sees
Putting your name on a reseller panel is the easy half. The hard half is deciding which screens exist for your customer at all, and enforcing that.
By Provibr
If you resell VPS or hosting, your customers eventually want a panel. They want to reboot their own machine at two in the morning instead of opening a ticket, and you want them to.
That turns into two questions that look like one and are not. Whose name is on the panel, and which parts of the panel exist for that person at all. The first is branding, and it is mostly presentation. The second is a boundary, and getting it wrong is how a customer of yours ends up reading your supplier invoice.
Branding stops where the paperwork starts
Rebranding a panel means changing the chrome: the wordmark in the sidebar, the page title, the favicon, the name in the account block. That is a useful amount, and it is where most white-label features stop.
The interesting part is the list of things that must never carry the reseller's name.
| Screen | Whose name it carries | Why |
|---|---|---|
| Panel chrome, titles, favicon | The reseller | The relationship the customer is in |
| Privacy statement | The platform | A processing notice describes who actually processes |
| Terms of service | The platform | It is the platform's agreement, not the reseller's |
| Data processing agreement | The platform | Same, and it names sub-processors |
| Invoice and proforma documents | The platform | It is a financial record of a real transaction |
| The line on a bank statement | The platform | A permanent record nobody can annotate later |
The reasoning is one sentence: a customer of your customer must never come to believe they have a contract with someone other than the party they actually have it with. Everything with a legal or financial character carries the name of the counterparty in that document, and that is not a design preference.
What matters more than the rule is where it lives. In our codebase it is a list of file paths, and a test walks the transitive import tree from each of them, failing the moment one can reach the branding module at all. Breadth-first, so the error names the shortest chain, which is the one to cut.
A direct-import check is too easy to slip past: put the brand lookup in a helper, import the helper, done. And the guard tests itself both ways. A listed path that disappears is an error rather than a silent skip, and it separately asserts that the panel layout does reach the branding module, because a resolver that quietly resolves nothing would mark every file green.
The reseller's own login is never rebranded
There is a second rule that is less obvious, and that we would have got wrong without stating it: branding applies only on the customer's own domain.
When the reseller logs in on the platform's own host, they see the platform, not their own brand. That is where invoices, the subscription and the relationship with us live. The brand lookup falls back to plain the moment a request is not bound to an account domain, so exactly one place decides and no screen has to remember.
The brand management screen shows a preview of the customer-facing chrome rather than being rebranded itself. That sounds pedantic until you consider what the alternative teaches: a reseller who sees their own logo above their own invoice will reasonably conclude that their customers see something similar.
A white logo on a white sidebar
Our panel background is white, so a logo with a transparent background and white ink is invisible, and the person uploading it will not find out: they are looking at it in a design tool on a grey artboard.
We measured rather than guessed, compositing each logo onto the actual surface colour in a headless browser:
| Logo | Contrast against the panel surface |
|---|---|
| Dark ink, transparent background | 16.46:1 |
| White ink, transparent background | 1.00:1 |
| White ink on its own opaque plate | 17.74:1 |
| Light grey ink | 1.24:1 |
The 1.00:1 row is not "hard to read". Every pixel is identical to the background. The logo is drawn and there is nothing to see.
Two decisions came out of that. There is deliberately no plate underneath the logo, because that would redraw the customer's brand and give every normal logo a frame in order to rescue one exception. And the upload screen warns rather than refuses, because a brand is allowed to be light grey.
The measurement counts ink, not overlap. A logo with a large transparent margin coincides with the background across most of its area and is perfectly visible; what matters is how much of it is drawn and how far that ink sits from the surface colour.
Absence is not a permission
Now the harder half. Which screens should exist for a customer of a reseller?
The instinct is to model this as permissions: give the sub-customer a role, tick fewer boxes. That is the wrong shape, for a reason that only shows up at the edges. Permissions answer "may this member do this". The question here is "does this screen exist on this domain", and the two come apart the moment an account owner logs in to look at their own customer panel. They are the owner. Every permission check waves them through.
So the model is an inversion rather than a right. A fixed list says which surfaces are absent on an account domain: billing, account settings, tickets, agents, integrations, the account activity log, the topology view, the platform administration area, and the sign-up and forgotten-password pages. Absent for everybody, including the owner.
Three of those need justification, because the reason is not the obvious one.
Tickets are absent because a customer of a reseller has a support relationship with the reseller, not with us. If that screen existed, they would read a reply naming a company that, according to their own panel, does not exist.
The activity log is absent because it is the account's log, not the reader's. The honest outcome is not a filtered log but "this screen does not exist here". The per-server activity tab stays, because that one really is scoped to a machine they have.
The topology view proves the model. It runs on the servers module, and the servers module is precisely the one thing a sub-customer does have. A permission gate would weed out nothing at all.
Three closures, because a page is not a boundary
One check is not enough, and each of the three catches what the others cannot.
The first is in the permission function itself, where the order of two lines is the whole point:
- Does this module exist on this surface? If not, refuse.
- Does this role have full access? If so, allow.
Putting the surface test first is what makes "does not exist" beat "may everything", and it is what makes the inversion hold everywhere rather than only in the screens somebody remembered.
The second is a guard in every page under those paths, producing a normal 404. It deliberately takes no argument: a call that named its own screen would let a typo become a silently open door.
The third is in the reverse proxy, because a page cannot catch a request that never reaches a page. A POST to an absent path is refused before any page code runs. Navigations are let through so that the application's own 404 renders instead of a bare proxy error.
The cookie is a browser rule, not a server rule
This is the finding worth carrying away even if you build none of the above.
Our session cookies are host-only. No Domain attribute, so a session on one customer domain is not sent to another, and not to the platform host either. That is correct and it is tested, including the negative case where enabling cross-subdomain cookies makes a Domain appear.
It is also not a boundary. The sub-customer holds their own session token and can send it wherever they like with a command-line HTTP client. Only a browser would have declined.
We measured what that allowed, and it was worse than expected: a sub-customer could create a brand new organisation with themselves as owner, complete with its own trial subscription, along two independent code paths reachable from four different entry points. Four organisations, counted in the database afterwards. On their own domain the same request was refused by the proxy, so the door was shut on one side and open on the other.
The repair was not to add checks to those two paths. It was to put the boundary at the single point every authenticated request passes through, and the invariant "a sub-customer never becomes the owner of an organisation" in the one function all creation paths share.
The same round found the general version of the problem elsewhere. An API key carries a role but not a surface. That was not exploitable, because the tab where a key is created does not exist on an account domain. But that is a hidden button, not a boundary, and the difference matters on the day somebody adds a second way to create keys. The fix refuses the principal rather than tagging the key.
No column says "this is a sub-customer"
One last design note, because it is the kind of decision that pays off quietly.
A sub-customer is an ordinary user row with a reserved e-mail address of the form username@<organisation-id>.invalid, plus an ordinary membership row with a narrow permission profile. There is no boolean column marking them, on purpose: a second source of truth can drift out of step with the address, and then two parts of the system disagree about who somebody is.
Because the organisation ID is inside the address, uniqueness comes free from the index that was already there, and the same username can exist under two resellers. The login form asks for a username and takes the organisation ID from the host rather than the form, so nobody signs in to a different account by guessing an identifier.
And their address is never marked as verified, because nobody can verify it. That column means "this address demonstrably belongs to this person", and setting it true for an address that reaches no mailbox would move the lie somewhere less visible.
What it does not do
Rebranding covers the chrome. The interface copy still reads as our product in whichever language the visitor uses, and pretending otherwise would mean translating a reseller's vocabulary into nine languages on their behalf. The absent-surface list is a decision about product shape, not a security model on its own: it works because the three closures behind it do.
This is how account domains work at Provibr, where the agent runs on your own hardware and connects outbound only.