Client Portal API

REST endpoints for the Client Portal module — portal projects, milestones, deliverables, messages, reports.

There are two API surfaces for portal data:

  • /api/portal/* — used by the tenant team (authenticated session).
  • /api/client/* — used by the client guest (magic-link session).

The split prevents accidentally exposing internal-only fields to the client view.

Team endpoints (/api/portal/*)

MethodPathPurpose
GET/api/portal/projectsList portal projects.
POST/api/portal/projectsCreate.
GET/PATCH/DELETE/api/portal/projects/{id}CRUD.
GET/api/portal/projects/{id}/messagesList messages including is_internal=true.
POST/api/portal/projects/{id}/messagesPost a message (may set is_internal).
GET/api/portal/projects/{id}/deliverablesList.
POST/api/portal/projects/{id}/deliverablesUpload + create.
GET/api/portal/projects/{id}/milestonesList.
POST/api/portal/projects/{id}/milestonesCreate.
GET/api/portal/projects/{id}/reportsList AI-generated reports.
POST/api/portal/file-requestsIssue a file-upload request to the client.
POST/api/portal/inviteSend a magic-link invite to a client.

Magic-link session (shared)

MethodPathPurpose
POST/api/portal/verifyExchange magic-link token for a portal_session cookie.
GET/api/portal/sessionCurrent session info.

Client guest endpoints (/api/client/*)

MethodPathPurpose
GET/api/client/projects/{id}Project detail (excludes internal-only fields).
GET/api/client/projects/{id}/messagesMessages (filtered to is_internal=false).
POST/api/client/projects/{id}/messagesPost a client message. Triggers internal notifications + emits portal.message.received.
GET/api/client/dashboardAggregated portal overview for the client landing page.

Key entity shapes

Portal project

{
  "id": "uuid",
  "name": "string",
  "project_id": "uuid?",
  "client_id": "uuid?",
  "status": "active | completed | on_hold",
  "start_date": "iso-8601?",
  "end_date": "iso-8601?",
  "progress": 0,
  "metadata": {}
}

Portal message

{
  "id": "uuid",
  "portal_project_id": "uuid",
  "sender_id": "uuid?",
  "sender_email": "string?",
  "content": "string",
  "attachments": [],
  "is_internal": false,
  "created_at": "iso-8601"
}

sender_id is set for team members; sender_email is set for client guests (no Supabase user row).

Permissions

Seeded slugs: portal.messages.read|create. Projects / deliverables / file-requests / invites all gate on portal.messages.* overlays plus tenant-admin checks — there are no separate seeded portal.projects.*, portal.file_requests.*, or portal.invite slugs.

Client-guest side: gated by the magic-link session cookie — no permission system on the client surface (every authenticated client sees the portal_project they were invited to).

Events

  • portal.message.sent (team-originated)
  • portal.message.received (client-originated)
  • portal.file.uploaded

Deliverable + project lifecycle events are tracked in the in-app activity feed but not emitted to the platform event bus today.

OpenAPI

Full schema at /api/openapi.json.