Skip to content

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.

Melbora tracks two distinct relationships between a Client and a Website. They coexist on the same Website row but are stored in different places.

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-id on vantage 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.

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.

Links carry one of two roles. The role gates write surface only — both roles see the same read surface.

RoleStatus + documents readCMS editsRedeploy
vieweryesnono
editoryesyesyes

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.

The flow has one branch, driven by whether the email already matches a Client record on file.

  1. 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.add in the SDK; vantage websites clients add in the CLI reference).
  2. If a Client with that email already exists, Melbora writes the WebsiteClientLink row immediately. The response carries { kind: "linked", link } and the client sees the Website on their next portal load. No email is sent.
  3. If no Client matches the email, Melbora creates an invitation with kind: "client_access" and emails the recipient a one-click link. The response carries { kind: "invited", invitation }.
  4. The recipient signs up through the invitation link, then the invitation wizard calls POST /invitations/:id/claim. That claim handler materializes the WebsiteClientLink (and marks the invitation accepted) — 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.

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 dueAt field 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.

The two flows answer different questions:

Add ClientTransfer ownership
GoalGive a stakeholder visibility / CMS accessHand the Website over to the client
BillingUnaffectedStripe subscription created / attached to the new owner
Website.clientIdUnchangedSet to the claiming client
StorageWebsiteClientLink rowWebsite.clientId field
ReversibilityRevoke link instantly, no billing side effectsRequires unassign + new transfer; touches Stripe
CardinalityMany per WebsiteAt most one owner per Website

Use Add Client for collaborators. Use Transfer for the moment you actually hand the project off.