Implementation Portal API Surface
Source-backed interface reference for apps/backend/src/implementation-portal/.
Use this page when wiring clients, MCP tools, import jobs, or support scripts. For domain rules and service responsibilities, read Implementation Portal Module first.
Scope
The implementation portal exposes three interface surfaces:
- team APIs for guide templates, boards, steps, and invoice drafts
- public/client portal APIs keyed by a board share token
- data-import handlers for templates, boards, time entries, and assignments
Paths below are controller paths. Prepend the backend prefix used by the running environment.
Interface and auth map
| Surface | Codepath | Auth model |
|---|---|---|
| Team guide templates | interfaces/http/guide-template.controller.ts | global app auth; most routes have @Permission(PolicyResource.GuideTemplate, ...) |
| Team implementation boards | interfaces/http/implementation-board.controller.ts | global app auth plus PolicyResource.ImplementationBoard permissions |
| Team board steps | interfaces/http/board-step.controller.ts | global app auth plus PolicyResource.BoardStep permissions |
| Team invoice drafts | interfaces/http/invoice-draft.controller.ts | global app auth plus PolicyResource.InvoiceDraft permissions |
| Client portal read/OTP | interfaces/http/client-portal.controller.ts | @IsPublic() on GET /portal/:shareToken, POST .../request-otp, and POST .../verify-otp |
| Client portal session check / writes | interfaces/http/client-portal.controller.ts | PortalSessionGuard only; routes are not @IsPublic() today |
| Bulk import | apps/backend/src/data-import/* | global app auth plus business-access checks in DataImportController |
Confirmed portal write auth behavior
Public portal read and OTP routes are marked @IsPublic(). These routes are
not:
GET /portal/:shareToken/auth/sessionPATCH /portal/:shareToken/steps/:stepId/completePATCH /portal/:shareToken/steps/:stepId/client-approvePOST /portal/:shareToken/steps/:stepId/attachmentsPOST /portal/:shareToken/steps/:stepId/comments
Those handlers use PortalSessionGuard, but the global Firebase AuthGuard
runs first. For an OTP-only client that sends only
x-portal-session-token / flowpos-portal-session (no Firebase ID token),
AuthGuard throws 401 Unauthorized with message No token provided
(apps/backend/src/auth/infrastructure/auth.guard.ts) before
PortalSessionGuard can validate the portal session.
Treat OTP-only portal writes and session checks as blocked by current
controller metadata, not as an intermittent integration issue. The intended
fix is to mark those handlers @IsPublic() so PortalSessionGuard becomes
the auth boundary for client portal mutations.
Team API groups
Guide templates
GuideTemplateController owns template metadata, template tree edits, cloning,
and guide resources.
| Method | Path | Purpose |
|---|---|---|
GET | /businesses/:businessId/guide-templates | List templates for a business |
POST | /businesses/:businessId/guide-templates | Create a guide template |
GET | /businesses/:businessId/guide-templates/:id | Get a template with phases and steps |
PUT | /businesses/:businessId/guide-templates/:id | Update template metadata |
DELETE | /businesses/:businessId/guide-templates/:id | Delete a template |
POST | /businesses/:businessId/guide-templates/:id/clone | Clone a template into an independent copy |
POST | /businesses/:businessId/guide-templates/:id/phases | Add a phase |
PUT | /businesses/:businessId/guide-templates/:id/phases/:phaseId | Update phase metadata/order |
DELETE | /businesses/:businessId/guide-templates/:id/phases/:phaseId | Delete a phase and its steps |
POST | /businesses/:businessId/guide-templates/:id/phases/:phaseId/steps | Add a step |
PUT | /businesses/:businessId/guide-templates/:id/phases/:phaseId/steps/:stepId | Update a step |
DELETE | /businesses/:businessId/guide-templates/:id/phases/:phaseId/steps/:stepId | Delete a step |
Guide resources attach implementation material to either phases or steps:
| Method | Path | Purpose |
|---|---|---|
GET | /businesses/:businessId/guide-templates/:id/phases/:phaseId/resources | List phase resources |
POST | /businesses/:businessId/guide-templates/:id/phases/:phaseId/resources | Add a phase resource (link, comment, or file) |
GET | /businesses/:businessId/guide-templates/:id/phases/:phaseId/steps/:stepId/resources | List step resources |
POST | /businesses/:businessId/guide-templates/:id/phases/:phaseId/steps/:stepId/resources | Add a step resource |
PUT | /businesses/:businessId/guide-templates/:id/resources/:resourceId | Update resource title, URL, or body |
DELETE | /businesses/:businessId/guide-templates/:id/resources/:resourceId | Delete a resource |
File resources use the storage port and return signed read URLs when listing. That signing behavior is an interface concern; guide resources remain domain entities independent of GCS.
Implementation boards
ImplementationBoardController exposes the team board lifecycle.
| Method | Path | Purpose |
|---|---|---|
GET | /businesses/:businessId/implementation-boards | List boards with health and completion stats |
POST | /businesses/:businessId/implementation-boards | Create a board, optionally from a template |
GET | /businesses/:businessId/implementation-boards/:id | Get board tree with stats |
PUT | /businesses/:businessId/implementation-boards/:id | Update board metadata |
DELETE | /businesses/:businessId/implementation-boards/:id | Archive a board |
PATCH | /businesses/:businessId/implementation-boards/:id/status | Move board through valid status transitions |
POST | /businesses/:businessId/implementation-boards/:id/regenerate-share-token | Rotate the client portal share token |
Board status transitions are domain behavior on ImplementationBoard:
draft -> active -> completed -> archived, with archive allowed from any
non-archived state.
Board steps
BoardStepController exposes team operations for execution, billing evidence,
files, and comments.
| Method | Path | Purpose |
|---|---|---|
PATCH | /businesses/:businessId/board-steps/:stepId/status | Start, complete, approve, reject, or return a step |
PATCH | /businesses/:businessId/board-steps/:stepId/assign | Assign a team member |
PATCH | /businesses/:businessId/board-steps/:stepId/due-date | Update due date |
GET | /businesses/:businessId/board-steps/:stepId/time-entries | List time entries |
POST | /businesses/:businessId/board-steps/:stepId/time-entries | Log hours on hourly-billed steps |
PUT | /businesses/:businessId/board-steps/:stepId/time-entries/:entryId | Edit a time entry |
DELETE | /businesses/:businessId/board-steps/:stepId/time-entries/:entryId | Delete a time entry |
GET | /businesses/:businessId/board-steps/:stepId/attachments | List attachments with signed URLs |
POST | /businesses/:businessId/board-steps/:stepId/attachments | Upload an attachment |
DELETE | /businesses/:businessId/board-steps/:stepId/attachments/:attachmentId | Delete an attachment |
GET | /businesses/:businessId/board-steps/:stepId/comments | List comments, including internal comments |
POST | /businesses/:businessId/board-steps/:stepId/comments | Post a team comment |
PUT | /businesses/:businessId/board-steps/:stepId/comments/:commentId | Edit own comment |
DELETE | /businesses/:businessId/board-steps/:stepId/comments/:commentId | Delete own comment |
Important constraints from domain/use-case code:
- step attachments are capped at 50 MB and must use an allowed MIME type
- hourly time entries require an hourly step and a minimum of
0.25hours - time-entry edit/delete is blocked once the step is linked to an invoice draft
- document-step completion requires at least one attachment
Invoice drafts
InvoiceDraftController exposes draft generation and lifecycle actions.
| Method | Path | Purpose |
|---|---|---|
GET | /businesses/:businessId/implementation-boards/:boardId/invoice-drafts | List drafts for a board |
POST | /businesses/:businessId/implementation-boards/:boardId/invoice-drafts | Generate a draft from billable steps |
GET | /businesses/:businessId/invoice-drafts/:id | Get a draft |
PATCH | /businesses/:businessId/invoice-drafts/:id/line-items/:stepId | Update line-item description |
POST | /businesses/:businessId/invoice-drafts/:id/send | Push to payment adapter and mark sent |
POST | /businesses/:businessId/invoice-drafts/:id/mark-paid | Manually mark paid |
POST | /businesses/:businessId/invoice-drafts/:id/void | Void a draft or sent invoice |
Billing eligibility is application/domain behavior in
InvoiceCalculatorService, not controller behavior.
Client portal API
ClientPortalController is intentionally narrower than the team API. It reads a
scrubbed board view and allows client actions only after OTP verification.
| Method | Path | Purpose |
|---|---|---|
GET | /portal/:shareToken | Public client board view without billing-sensitive fields |
POST | /portal/:shareToken/auth/request-otp | Request an email OTP; always returns 204 to avoid email enumeration |
POST | /portal/:shareToken/auth/verify-otp | Verify OTP and receive a portal session token |
GET | /portal/:shareToken/auth/session | Validate the current portal session header or cookie; returns 204 when valid |
PATCH | /portal/:shareToken/steps/:stepId/complete | Complete a client-assigned step |
PATCH | /portal/:shareToken/steps/:stepId/client-approve | Client-approve a completed implementer step |
POST | /portal/:shareToken/steps/:stepId/attachments | Client upload for document steps; multipart body must include file and boardId |
POST | /portal/:shareToken/steps/:stepId/comments | Client comment, always persisted as client_visible |
Portal session tokens can be sent in:
x-portal-session-tokenflowpos-portal-session
verify-otp returns the session token in the JSON response and also sets the
flowpos-portal-session cookie. In production, the cookie is httpOnly,
secure, and sameSite=none; in non-production it uses sameSite=lax.
Data import surface
Implementation Portal imports are registered in DataImportModule, not in the
implementation portal module itself. The import handlers still call the
implementation portal application services and repositories.
Supported import types:
| Import type | Required template headers |
|---|---|
implementation-portal-template | templateName, templateDescription, vertical, phaseName, phaseDescription, phaseOrder, stepName, stepDescription, stepOrder, stepType, assigneeRole, billableType, fixedAmount, hourlyRate, dueOffsetDays, isVisibleToClient |
implementation-portal-board | clientName, clientEmail, templateName, startDate |
implementation-portal-time-entry | boardName, stepName, hours, description, loggedAt, externalId |
implementation-portal-step-assignment | boardName, stepName, assignedTo, authorizedBy |
Use GET /data-import/templates/:importType to download generated templates.
Use the normal data-import upload/preview endpoints for execution; the import
controller enforces business access from the authenticated Firebase user before
running a job.