SaaS-Stack integration
Sitebin Enterprise is a first-class app of the open-source IT-Trail SaaS Stack — onboard it like any of your apps and it inherits single sign-on, consent, subscription billing and GDPR self-service across your whole portfolio.
What you get
- Single sign-on — users log in to Sitebin with the same identity they use for every other app on your stack, via the stack's Auth Gateway.
- Consent at sign-in — the stack asks each user to accept your platform's terms once, and then Sitebin's own documents (on sitebin.io: the terms of service and the data processing agreement), on a page it hosts inside the sign-in flow. Sitebin renders no consent screen of its own.
- One subscription — the tier a user bought through the stack (PayGate, in front of whatever processor the stack uses) governs their Sitebin quotas. Sitebin never learns which processor that is.
- One place to manage it — "Manage account" and "Manage subscription" on the Sitebin dashboard are links to the stack's account console and hosted plan page, not screens of Sitebin's own.
- One erasure — a user who deletes their account or asks for their data at the stack has the order carried into Sitebin by two signed webhooks.
- One suspension — an operator who suspends an abuser in the stack shuts them out of Sitebin too: every way in is refused and every site they own is locked, by a third signed webhook.
1 · Let Sitebin onboard itself
There is no app to create by hand. Give Sitebin the stack URL, an app id and the stack's platform admin key, and it announces itself to the Platform API on every start:
SITEBIN_STACK_URL=https://platform.saas-stack.example.com
SITEBIN_STACK_APP_ID=sitebin
SITEBIN_STACK_ADMIN_KEY_FILE=/run/secrets/stack_admin_key # the stack's PLATFORM_ADMIN_KEY, from a docker secret
# (or SITEBIN_STACK_ADMIN_KEY=… — either way it is scrubbed from the process environment once read)
# this deployment's consent documents, asked at sign-in in this order after the platform's own
SITEBIN_STACK_CONSENTS='[{"key":"terms","version":"2026-09-29","url":"https://sitebin.io/terms/","title":{"en":"Sitebin Terms of Service"}},{"key":"dpa","version":"2026-09-29","url":"https://sitebin.io/dpa/","title":{"en":"Data Processing Agreement"}}]'
# the secret the stack signs its GDPR orders with — required whenever SITEBIN_STACK_URL is set
SITEBIN_STACK_GDPR_SECRET=… # at least 32 characters
The declaration is convergent — run it a thousand times and the stack simply matches what Sitebin declared — and it never blocks startup: an unreachable stack logs an error, Sitebin serves sites anyway, and the next start converges again. Configuration by deploying, not by clicking.
What it declares is only what Sitebin alone knows: its identity, the
OIDC callback it will actually use
(https://sitebin.example.com/account/auth/oidc/callback), the
page a sign-out returns to
(postLogoutRedirectUris:
https://sitebin.example.com/account/signed-out), its
tier catalogue from tiers.json, its consent documents, its
GDPR endpoints, and — where MCP is served with an OAuth issuer — its MCP
resource and scopes. It never declares identity providers, password
policy, MFA or realm registration: those are realm-wide settings shared
with every app on the stack, and an app that set them would overwrite an
operator's choice on each restart. Of a tier it declares the
amount, never a price identifier — the stack creates
the product in whatever processor it uses, and a tier with no amount
declares no payment product, which is how free plans (and plans whose
pricing is not settled) announce themselves. Registration
merges: a block that is absent leaves what the stack already
holds untouched.
The admin key is the stack's master credential. Prefer
SITEBIN_STACK_ADMIN_KEY_FILE — a docker secret or mounted
file — so it never sits in the container's environment where
docker inspect and every child process could read it.
2 · Point Sitebin at the stack
# SSO through the stack's Auth Gateway (generic OIDC) # The ISSUER is the realm — it is what `iss` says. The DISCOVERY URL is the # gateway, and it is what puts sign-in behind the stack's consent gate. SITEBIN_OAUTH_OIDC_ISSUER=https://auth.example.com/realms/saas-stack SITEBIN_OAUTH_OIDC_DISCOVERY_URL=https://auth-gw.saas-stack.example.com/api/v1/sitebin SITEBIN_OAUTH_OIDC_CLIENT_ID=sitebin-app SITEBIN_OAUTH_OIDC_CLIENT_SECRET=… SITEBIN_OAUTH_OIDC_LABEL="Example SSO" # login-button text # (recommended) SSO only — local accounts would bypass PayGate billing SITEBIN_LOCAL_AUTH=false # Tiers from PayGate (requires SITEBIN_ACCOUNT_MODE=tiers) SITEBIN_BILLING=paygate # which backend may charge customers SITEBIN_PAYGATE_URL=https://paygate.saas-stack.example.com SITEBIN_PAYGATE_APP_ID=sitebin SITEBIN_PAYGATE_API_KEY=ssk_live_… SITEBIN_PAYGATE_CACHE_TTL=5m # per-user tier cache # "Manage subscription" -> https://auth.example.com/apps/sitebin/plan — derived, nothing to set
issuer must still equal
SITEBIN_OAUTH_OIDC_ISSUER, so this cannot trust another
realm by accident.3 · Match the tier ids
PayGate decides which tier a user has; Sitebin's
tiers.json decides what that tier means (sites,
storage, files, domains, WebDAV). The convention is simply that
stack tier ids equal tiers.json ids — a
user with the stack subscription pro gets the
tiers.json tier pro. There is no mapping
table to maintain.
Consent: two documents, asked at sign-in
Consent is the stack's, not Sitebin's. The stack gates sign-in on the
platform's own terms, once per user, and then on each document the app
declares, once per app — on a page it hosts inside the sign-in flow,
themed and localized. Sitebin's whole side of it is
SITEBIN_STACK_CONSENTS: an ordered list of documents, each
with a key, a version, a url and
an optional title map by locale. There is no page to
render, no endpoint to expose and no callback to implement.
keyis the document's identity for ever; changing it declares a new document and asks everybody again.versionis opaque and immutable once recorded: raising it re-asks every user for that document — per document, so the DPA's version can move without touching the terms' — and re-declaring a recorded version with different content is refused.requireddefaults to true on the stack;falsemakes a document that is shown and recorded but does not block (a marketing consent).- Absent = declare nothing, and the stack keeps whatever it already holds. An empty list is refused at startup: it would mean "this app asks for nothing", a real state an operator sets on the stack on purpose.
Two things have to be true for a user to actually see the documents:
the variable is set, and SITEBIN_OAUTH_OIDC_DISCOVERY_URL
points at the Auth Gateway, as above.
Self-service on the stack's pages
Once a user is signed in through the stack, "manage my account" and "manage my plan" are links, not screens — and both addresses are derived from the issuer the instance signs in against, exactly as the stack's own SDK builds them. There is nothing to configure, so nothing that can disagree with the stack.
- Manage account on the dashboard is the stack's
account console,
<issuer>/account/?referrer=<client id>: password, sign-in methods, sessions and devices, second factors, data export and account deletion. - Manage subscription posts to
/account/billing/portal, which with PayGate redirects to the stack's hosted plan page,<issuer origin>/apps/<app id>/plan: current plan, the plans it could move to, change, cancel, resume, invoices — themed as Sitebin and built by nobody here. A plan change is an order like a first purchase: Upgrade for a customer who already pays opens the stack's order page for exactly that change, with the new price, what is due today and an order button that names the obligation to pay, and the stack mails the confirmation. - Deleting a stack account happens at the console, not in
Sitebin. The dashboard's danger zone sends a stack user
there, and a
POST /account/deletefrom a stack account redirects there too. The stack erases the identity and orders Sitebin to erase its half (next section). Local accounts, which the stack has never heard of, keep local deletion: a confirmation page of its own, and with a direct Stripe/Paddle backend the live subscription is cancelled first, with immediate effect — if it cannot be, the account is kept and the page says why. - Sign out ends the stack session too. Sitebin
revokes every session of the account, then sends the browser through
the stack's
end_session_endpointwithclient_id, the sign-in's ID token asid_token_hintandpost_logout_redirect_uri=<base>/account/signed-out. The stack ends the single sign-on without asking and returns to "You are signed out", whose Sign in starts a fresh request (prompt=login) — so the stack's login page appears and a different account can be chosen. Without the hint (a session older than the week it is kept) the stack may ask "Do you want to log out?" first. Signing in from the stack's hub stays one click.
auth.postLogoutRedirectUris, and the stack's registration
schema is strict: an instance that declares it to a stack that does not
know the field is refused registration (it logs the refusal and keeps
serving). Run a stack that knows it before you upgrade Sitebin past
it.GDPR: the stack orders, Sitebin erases
A data subject asks the stack — in the account console, or an operator
does it for them in the stack's user directory — and the stack calls
every app the person belongs to. These, and the
suspension below, are the only calls
the stack ever makes into a Sitebin instance. Sitebin's side is
two endpoints, mounted and declared as the registration's
gdpr block when SITEBIN_STACK_GDPR_SECRET is
set — and without the secret neither exists, because a URL the stack
could call but nothing could verify would be an unauthenticated
deletion endpoint.
| Endpoint | What it does |
|---|---|
POST /account/gdpr/export | Everything this instance holds about the user, as JSON: the account record (never a password hash), the metadata of every site it owns (id, URL, mode, custom domains, origin, size, files, created, expires), the metadata of every API token (never a secret — none is stored), what is held about sessions (nothing: they are signed cookies the browser keeps), and the account's provenance — its own log (sign-up, sign-ins, tokens minted, sites created and deleted) as provenance.account, and from each site it owns the entries this account made as provenance.sites. Entries made on its sites with an edit password are not exported: nothing says they were this person. An unknown user exports an empty document, not an error. |
POST /account/gdpr/delete | Removes the account, its ownership markers, its sites (with their provenance logs), its account provenance log, its API tokens and — by removing the record every cookie is validated against — its sessions. Idempotent: a user with no account here is a 200, because the stack reads every other status, 404 included, as "the app still holds the data" and never deletes the identity. A site that cannot be deleted stops the order with a 500 and keeps the account, so the stack keeps the identity and the operator retries; whatever was deleted before stays deleted and the retry steps over it. An account that owns a site the operator has locked is refused whole with a 409 naming the sites, and nothing is deleted: the lock is an evidence hold, released only by the operator deleting those sites. |
Both take {"userId": "<stack user id>", "email": "…"}
and are authenticated by nothing but the signature:
X-Signature: sha256=<hex HMAC-SHA256 over "<X-Timestamp>.<body>">
with the shared secret, compared in constant time, the timestamp (Unix
seconds) accepted within five minutes either way. No session, admin key
or API token is accepted there, and a request that fails verification
is refused before its body is parsed. The user id is the OIDC subject;
the stack knows nothing about local accounts and never orders anything
for them.
Suspension: the stack shuts an abuser out
The stack's Suspend user disables the identity, ends its sessions and revokes its offline tokens — but Sitebin's own session cookie, its API tokens and its MCP tokens do not ask the identity provider. So the stack also calls every app the user belongs to, and Sitebin takes the order through the same door as the GDPR ones:
| Endpoint | What it does |
|---|---|
POST /account/gdpr/suspend | Declared to the stack as gdpr.suspendUserUrl — https://<base domain>/account/gdpr/suspend — next to the delete and export URLs, and like them mounted and declared only when SITEBIN_STACK_GDPR_SECRET is set. Body {"userId", "email", "suspended": true|false, "reason"}, signed exactly like the GDPR orders above. |
"suspended": truemarks the account suspended (a repeated order keeps the first date), ends every browser session at once, and from then on refuses the account on every path: sign-in, the dashboard and the owner's edit page, account API tokens, and MCP OAuth tokens — checked after a token resolves, so no token hands the account back. Every site it owns is locked with the reason: served to nobody, frozen, kept past its expiry — up to the lock retention."suspended": falseclears the suspension and unlocks only the sites the suspension locked. A site the operator locked by hand — or kept locked with Keep in the register — stays locked.- A user with no account on this instance is a
200, so the order is idempotent; a body withoutsuspendedis a400, never a guess; a lock or unlock that fails is a5xx, and because the account's own state is written first, the stack's retry converges. - The instance register badges the owner of such sites Suspended. A suspension does not cancel a paid subscription; that stays a billing decision in the stack.
suspendUserUrl to a stack that does not know
the field is refused registration (it logs the refusal and keeps
serving). Run a stack built with user suspension before you upgrade
Sitebin past it.How resolution behaves
- Tiers resolve via PayGate only for accounts signed in through the
generic OIDC provider — their OIDC subject is the stack
user id. Local, Google and Microsoft accounts keep Sitebin's stored
tier, and a local account has no subscription on the stack and never
will — which is why
SITEBIN_LOCAL_AUTH=falseis the recommendation on a stack instance. - Lookups use PayGate's admin-by-user-id endpoint with the app API key — Sitebin never stores user JWTs.
active,trialingandpast_duesubscriptions are honored; anything else (and unknown tier ids) falls back to the account's stored tier, thenSITEBIN_DEFAULT_TIER.- Results are cached per user (
SITEBIN_PAYGATE_CACHE_TTL, default 5 minutes; failures 30 seconds) and fail open: a PayGate outage never blocks publishing. - Exactly one billing backend runs. PayGate and the
built-in Stripe/Paddle checkout are alternatives, not layers:
SITEBIN_BILLINGnames the one that may charge customers (paygate,stripeorpaddle). With only one configured it is inferred and you can leave the variable out; configure two and start without it and Sitebin refuses to start rather than guess which processor bills your users. - With PayGate as the backend,
SITEBIN_TIER_SELF_SELECTis bypassed for SSO accounts — the stack owns their subscription.