Bring your own portal
Build and host your own customer portal on Holaboss using the publishable key and the @holaboss/client SDK.
Guides
Your customer-facing portal doesn't have to be the iMerch template. The publishable key and the
@holaboss/client SDK are the portal surface — the template is just one app that uses them. Point
your own app at the same two things and it works.
The short answer — yes. A portal you build and host yourself works, as long as it (1) signs users in with your org's publishable key and (2) talks to Holaboss through the SDK with the resulting session token. Those two pieces are decoupled from the template on purpose. What you give up by self-hosting is the console's deploy / analytics / domain conveniences — not the runtime.
The model
There are two layers, and it helps to keep them apart:
- The runtime surface — the publishable key +
@holaboss/client. This is what a customer's browser uses to sign in, read the org's data, upload files, and chat. It's a public, bring-your-own surface: run it on any stack. - The hosting pipeline — the console's create → preview → publish → analytics → custom-domain flow. That part is specific to the iMerch template: it clones the starter repo, bakes your brand, and deploys a Cloudflare Worker for you. A self-hosted app lives outside it.
The publishable key is safe to ship in the browser. It only says which org's user pool a request belongs to — every authorization decision comes from the session token, never the key.
1 · Sign in
Sign-in is a passwordless email code, scoped to your org by the publishable key. Two calls: request a code, then redeem it for a session.
// 1 — Ask for a code. The publishable key scopes it to your org's user pool.
await requestCode(PUBLISHABLE_KEY, email);
// → POST /auth/otp { publishable_key, email }
// Always "succeeds" — it never reveals whether the address is a known user.
// 2 — Redeem the code for a session.
const session = await redeemCode(PUBLISHABLE_KEY, email, code);
// → POST /auth/verify { publishable_key, email, code }
// → { access_token, refresh_token, expires_in, end_user: { id, email } }Store the session (the reference template keeps it in localStorage — fine, since auth is a bearer
token, not a cookie). Access tokens are short-lived, so refresh ahead of expiry and rotate the
refresh token on each use.
2 · Read & write data
After sign-in the key steps aside — everything runs on the session bearer. Hand the SDK a token getter
that returns a fresh access token, and it does the rest.
import { createApiTransport, createPortalData } from "@holaboss/client";
const config = {
baseUrl: "", // same-origin — your proxy answers /api/*
token: async () => (await currentAccessToken()) ?? "",
};
export const chat = createApiTransport(config); // the customer's one thread
export const data = createPortalData(config); // tables + filesdata.listRows("orders"), data.fileLink(id), chat.getInfo() — the same surface the template uses.
Build whatever UI you like on top.
Reaching the gateway
The SDK builds paths like /api/v1/end-user/… and expects them to reach the Holaboss gateway's end-user
surface (/gateway/endUser/*). You have two ways to connect it.
Option A — same-origin proxy (what the template does)
A tiny edge worker forwards /api/* to the gateway. No CORS, no cross-site cookie questions, and the day
you move to a custom domain, only DNS changes.
if (url.pathname.startsWith("/api/")) {
const upstream = `${env.HOLABOSS_GATEWAY}/gateway/endUser/${rest}`;
// forward ONLY: authorization, content-type, accept.
// never pass through cookies or a client-set identity header.
return fetch(upstream, { method, headers, body });
}
return env.ASSETS.fetch(request); // otherwise serve your appOption B — call the gateway cross-origin
Set baseUrl to the gateway directly. It works because auth is an Authorization header, not a cookie —
but the end-user gateway has to allow your origin (CORS). The proxy avoids that entirely, so prefer
Option A unless you have a reason not to.
Header hygiene: forward an allowlist (authorization, content-type, accept) — never copy
arbitrary client headers through, or a caller could smuggle an identity header to the gateway.
What your app can see
Through the SDK your app reaches the org's portal-visible tables — orders, services, files, client tasks, and each running service's own data — the same slices the template shows. Access is gated by the table permission model (a table is readable/writable to customers only if it's marked so), and every request is still authorized by the session token. You can't reach anything a customer isn't allowed to.
Hosted vs. bring-your-own
The runtime is fully portable; the console's management features are wired to the template's Worker. Here's the split:
| Capability | iMerch template (hosted) | Your app (self-hosted) |
|---|---|---|
| Email-code sign-in (key) | ✓ yes | ✓ yes |
| Tables · files · chat (SDK) | ✓ yes | ✓ yes |
| Console create / preview / deploy | ✓ yes | — you deploy it |
| Analytics in the console | ✓ yes | — not your worker |
| Custom domain via console | ✓ yes | — you manage DNS |
| Where it runs | our Cloudflare Worker | your infrastructure |
Two things to set right.
Analytics won't cover a self-hosted app — the console reads Cloudflare metrics off our
portal-<slug> worker, so a self-hosted app registers as an external provider ("BYO — we don't
deploy it"). Bring your own analytics.
Point the portal URL at your app — set the org's portal URL to your own address, so OTP and invitation emails link customers to your app rather than the hosted template.
Ship checklist
- Grab the publishable key — Console → your App → Access & sign-in. It's public, safe to bake into the browser bundle.
- Wire email-code sign-in —
requestCode→redeemCodewith the key. Store the session; refresh the access token ahead of expiry. - Point the SDK at the gateway — proxy
/api/*same-origin (recommended), or setbaseUrlto the gateway with CORS. - Read & write the org's data —
createPortalDatafor tables and files,createApiTransportfor chat — within the table permissions. - Set the org's portal URL — so customer emails (OTP, invitations) link to your app, not the hosted template.
That's the whole contract. The key and the SDK give you a working portal on your own stack; the only thing reserved for the template is the console's hands-off deploy, analytics, and domain management.