Client Portal module

The Client Portal module is a branded surface for tenants to collaborate with external clients — share deliverables, exchange messages, request file uploads, send report PDFs. Clients authenticate via email-magic-link, not full tenant logins, so inviting a client doesn't consume a seat.

Entities

EntityWhat it represents
Portal projectA workspace for one client engagement. Has name, status (active/completed/on_hold), optional link to an internal projects.id, optional client_id for a registered client user.
Portal milestoneVisible roadmap entry on the portal page. Has title, due_date, status.
Portal deliverableA file delivered to the client. Has title, file URL, status (pending_review/approved/revision_requested/delivered), reviewer feedback.
Portal messageA message exchanged between the team and the client. Has sender_id (team) or sender_email (client guest), is_internal flag for team-only notes.
Portal reportAn AI-generated status report scoped to the portal project.
Portal file requestA "please upload" request issued to the client; has expiry, status.
Portal notificationIn-app notifications to the team when client activity arrives.

Schema under portal with public.* shims.

Client experience

Clients land on client.<basedomain>/projects/[id] after clicking their magic-link email. From there:

  • Timeline — milestones + deliverables in chronological order.
  • Messages — chat with the team. Internal-only notes from team members aren't visible.
  • Files — deliverables to download, requested files to upload.
  • Reports — AI-generated status updates pinned to the project.

No tenant nav, no module access — clients see only the portal.

Inbound client messages

When a client posts a message at POST /api/client/projects/[id]/messages:

  1. Row inserts into portal_messages with sender_id=null + sender_email=<client> + is_internal=false.
  2. We resolve internal recipients via the linked projects.project_idproject_members.user_id, falling back to tenant admins if no link.
  3. Enqueue a send-notification worker job → notifications rows land for every team member.
  4. Emit portal.message.received so webhook subscribers fire.

All three are fire-and-forget — a notification failure doesn't fail the message post; the message is durable in portal_messages.

Branding

Settings → Branding lets a tenant override the portal's logo + primary color + custom domain. The portal subdomain client.<basedomain> serves every tenant; custom domains require DNS setup + are gated to the Enterprise tier.

Deliverable review

Operator uploads a deliverable → status pending_review. Client opens the portal → can approve or request_revision with feedback text. Approved deliverables flip to delivered and appear in the client's "completed" list.

File requests

Operator clicks Request file → enters title + due date. Portal shows the client a one-time upload widget. Captured file lives in the tenant's storage bucket; the request's status flips on upload.

Cross-module insights

  • Portal × Projects: the linked internal project's task list
    • activity feed inform the portal's status view (without exposing internal task detail to the client).
  • Portal × Finance: deliverable approval can auto-issue an invoice line item; this is opt-in per portal-project.
  • Portal × CRM: every portal client is backed by a CRM contact; messaging captured as activities on the contact timeline.

Permissions

Portal access for team members: portal.projects.read|update, portal.messages.send. Internal-only notes need the same role + the team-member-of-this-project membership.

Client guest authentication: tenant-issued magic links signed with PORTAL_SESSION_SECRET. Sessions are 7-day TTL.

Read next

  • Portal API — team + client-guest endpoint split.
  • CRM module — every portal client is a CRM contact under the hood.