Client portal
import { Aside } from ‘@astrojs/starlight/components’;
A Website in Melbora can be reached by two kinds of non-admin account: the owning client (one per Website) and any number of linked clients added via the “Add Client” flow. They look the same to the client — both land in the client portal — but they’re issued through different code paths and grant different powers.
Two relationships, not one
Section titled “Two relationships, not one”Melbora tracks two distinct relationships between a Client and a Website. They coexist on the same Website row but are stored in different places.
Ownership
Section titled “Ownership”The owning client is the billing payer. There is at most one per Website. Ownership is set by:
- Transfer flow — admin issues a transfer invitation; client claims
it; on claim, Melbora runs the Stripe handshake (creates / attaches
the subscription) and writes
Website.clientId = <newClientId>. - Direct assignment at create-time via
--client-idonvantage websites create(for clients that are already on file and already have a payment method).
Owners see the Website with via: "owner" on the access ladder. They
have the full client surface — status, documents, analytics, marketing
— plus the Stripe-backed billing surface (invoices, billing portal,
subscription state). There is no role distinction for owners; they
always have the broader surface.
The transfer flow is documented separately — see the
WebsitesTransferApi reference
and vantage websites transfer in the
CLI reference.
Client access (“Add Client”)
Section titled “Client access (“Add Client”)”A separate flow that grants a client access to a Website without moving billing or ownership. The developer keeps the Website on their own books; the client gets a read (or read+CMS) seat.
Stored as a WebsiteClientLink row keyed by (websiteId, clientId),
each with a role. Linked clients see the Website with via: "link"
on the access ladder, and the link’s role determines write scope.
There is no billing side effect from adding a client this way. No Stripe call is made. Removing the link revokes access immediately and doesn’t touch any subscription.
The two access roles
Section titled “The two access roles”Links carry one of two roles. The role gates write surface only — both roles see the same read surface.
| Role | Status + documents read | CMS edits | Redeploy |
|---|---|---|---|
viewer | yes | no | no |
editor | yes | yes | yes |
Server-side, requireWebsiteEditor enforces the role on every
write route a linked client could reach (CMS content + media +
redeploy). Owners and admins always pass; viewers get a 401.
Pick viewer for stakeholders who only need visibility (a brand
manager who wants to download the contract, a partner tracking
delivery progress). Pick editor when the client needs to maintain
copy or product content through the CMS surface.
Owners always behave as editor regardless — there is no viewer
ownership. The role field only applies to linked clients.
Adding a client
Section titled “Adding a client”The flow has one branch, driven by whether the email already matches a Client record on file.
- Admin enters the client’s email + picks a role in the portal’s
Clients panel for the Website. This corresponds to
POST /websites/:id/clients(WebsitesClientAccessApi.addin the SDK;vantage websites clients addin the CLI reference). - If a Client with that email already exists, Melbora writes the
WebsiteClientLinkrow immediately. The response carries{ kind: "linked", link }and the client sees the Website on their next portal load. No email is sent. - If no Client matches the email, Melbora creates an
invitationwithkind: "client_access"and emails the recipient a one-click link. The response carries{ kind: "invited", invitation }. - The recipient signs up through the invitation link, then the
invitation wizard calls
POST /invitations/:id/claim. That claim handler materializes theWebsiteClientLink(and marks the invitationaccepted) — the link only exists once the recipient is signed in as the matching Cognito user.
Pending invitations are listed alongside active links so the admin
sees the complete “who has access” picture. Revoking a pending
invitation (DELETE /websites/:id/client-invitations/:invitationId)
prevents the link from being created on claim.
What clients see
Section titled “What clients see”Both owners and linked clients see the same read surface in the client portal:
- Status — the delivery block. ETA, ordered checkpoints with
current status, optional per-checkpoint deadlines (the
dueAtfield surfaces as “Due Aug 14” on pending rows, a “due …” subtext on in-progress rows, and is superseded by the completion timestamp on done rows), and (when the admin has enabled it) the live preview URL. The progress bar above the timeline shows three segments — done, in-progress (animated diagonal stripes), and pending. See Manage project delivery for the admin side. - Documents — finalized project documents. Clients can preview PDFs and images in-app via a modal viewer (click the filename), or download via a short-lived presigned URL. Unsupported file types fall back to a download-only placeholder. Writes (upload, rename, delete) remain admin-only. See Share documents with clients for the admin side.
- Analytics — the same SEO + traffic dashboards an admin sees for the Website. See Analytics.
- Marketing — content surfaces the developer has configured for the Website.
Owners additionally see the billing surface (invoices, Stripe portal link, subscription state). Linked clients don’t.
Editors additionally see the CMS surface. Viewers don’t.
Distinction from “Transfer ownership”
Section titled “Distinction from “Transfer ownership””The two flows answer different questions:
| Add Client | Transfer ownership | |
|---|---|---|
| Goal | Give a stakeholder visibility / CMS access | Hand the Website over to the client |
| Billing | Unaffected | Stripe subscription created / attached to the new owner |
Website.clientId | Unchanged | Set to the claiming client |
| Storage | WebsiteClientLink row | Website.clientId field |
| Reversibility | Revoke link instantly, no billing side effects | Requires unassign + new transfer; touches Stripe |
| Cardinality | Many per Website | At most one owner per Website |
Use Add Client for collaborators. Use Transfer for the moment you actually hand the project off.
See also
Section titled “See also”- Manage project delivery — the admin side of the Status page.
- Share documents with clients — the admin side of the Documents page.
- Websites — the underlying entity and its access model.
WebsitesClientAccessApireference — full SDK signatures.WebsitesTransferApireference — the ownership-transfer counterpart.