diff --git a/API-SPEC.yaml b/API-SPEC.yaml
new file mode 100644
index 0000000..c58d47c
--- /dev/null
+++ b/API-SPEC.yaml
@@ -0,0 +1,2909 @@
+openapi: 3.0.3
+info:
+ title: Dealspace API
+ description: |
+ M&A deal management platform API. Investment Banks, Sellers, and Buyers collaborate
+ on a structured request-and-answer workflow system.
+
+ ## Authentication
+ All endpoints except `/health` and `/api/auth/*` require a valid Bearer token.
+ Tokens are JWT (HS256) with 1-hour access token lifetime and 7-day refresh token lifetime.
+
+ ## RBAC Model
+ Access is scoped to projects and workstreams. Roles determine permissions:
+ - **ib_admin** — Full control over project and all workstreams
+ - **ib_member** — Manage requests and vet answers in assigned workstreams
+ - **seller_admin** — Manage seller team, see all seller-directed requests
+ - **seller_member** — Answer requests in assigned workstreams
+ - **buyer_admin** — Manage buyer team, access data room
+ - **buyer_member** — Submit requests, view published answers
+ - **observer** — Read-only access to assigned workstreams
+
+ ## Error Responses
+ All errors follow a consistent format:
+ ```json
+ {"error": "Human-readable message", "code": "ERROR_CODE", "details": {}}
+ ```
+
+ ## Rate Limiting
+ Endpoints are rate-limited per user, IP, and project. Headers `X-RateLimit-Limit`,
+ `X-RateLimit-Remaining`, and `Retry-After` are included in responses.
+ version: 1.0.0
+ contact:
+ name: Dealspace Support
+ email: support@muskepo.com
+ license:
+ name: Proprietary
+
+servers:
+ - url: https://muskepo.com/api
+ description: Production
+ - url: http://localhost:8080/api
+ description: Development
+
+tags:
+ - name: Auth
+ description: Authentication and MFA
+ - name: Projects
+ description: Top-level deal containers
+ - name: Workstreams
+ description: RBAC-anchored workflow containers (Finance, Legal, IT, etc.)
+ - name: Request Lists
+ description: Named collections of requests within a workstream
+ - name: Requests
+ description: Individual request items
+ - name: Answers
+ description: Responses to requests
+ - name: Matches
+ description: AI-suggested answer-to-request matching
+ - name: Objects
+ description: File upload/download with watermarking
+ - name: Tasks
+ description: Personal task inbox
+ - name: Access
+ description: RBAC grants
+ - name: Invites
+ description: Project invitations
+ - name: Audit
+ description: Security audit log
+ - name: Health
+ description: Service health check
+
+paths:
+ # ============================================================================
+ # AUTH ENDPOINTS
+ # ============================================================================
+
+ /auth/login:
+ post:
+ tags: [Auth]
+ summary: Authenticate with email and password
+ description: |
+ Returns access and refresh tokens. If MFA is required but not yet verified,
+ the token will have `mfa: false` and can only be used on `/auth/mfa/*` endpoints.
+
+ **Rate limit:** 5 requests/minute per IP
+ operationId: login
+ security: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required: [email, password]
+ properties:
+ email:
+ type: string
+ format: email
+ example: user@example.com
+ password:
+ type: string
+ format: password
+ minLength: 8
+ fingerprint:
+ type: object
+ description: Device fingerprint for session binding
+ properties:
+ userAgent:
+ type: string
+ acceptLanguage:
+ type: string
+ timezone:
+ type: string
+ responses:
+ '200':
+ description: Authentication successful
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthTokens'
+ '401':
+ description: Invalid credentials
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '429':
+ description: Rate limit exceeded
+ headers:
+ Retry-After:
+ schema:
+ type: integer
+ description: Seconds until rate limit resets
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ /auth/refresh:
+ post:
+ tags: [Auth]
+ summary: Rotate refresh token
+ description: |
+ Exchange a valid refresh token for new access and refresh tokens.
+ The old refresh token is invalidated (rotation). Fingerprint must match
+ the original session.
+
+ **Rate limit:** 60 requests/minute per user
+ operationId: refreshToken
+ security: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required: [refresh_token]
+ properties:
+ refresh_token:
+ type: string
+ fingerprint:
+ type: object
+ properties:
+ userAgent:
+ type: string
+ acceptLanguage:
+ type: string
+ timezone:
+ type: string
+ responses:
+ '200':
+ description: Tokens rotated successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthTokens'
+ '401':
+ description: Invalid or expired refresh token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ /auth/logout:
+ post:
+ tags: [Auth]
+ summary: Revoke current session
+ description: |
+ Invalidates the current access token and all associated refresh tokens.
+
+ **Rate limit:** 60 requests/minute per user
+ operationId: logout
+ security:
+ - bearerAuth: []
+ responses:
+ '204':
+ description: Session revoked successfully
+ '401':
+ description: Invalid or expired token
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ /auth/mfa/setup:
+ post:
+ tags: [Auth]
+ summary: Initiate TOTP MFA setup
+ description: |
+ Generates a new TOTP secret and returns a QR code URI for authenticator apps.
+ Also returns recovery codes (store securely, shown only once).
+
+ **RBAC:** Any authenticated user
+ **Rate limit:** 5 requests/minute per user
+ operationId: mfaSetup
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: MFA setup initiated
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ secret:
+ type: string
+ description: Base32-encoded TOTP secret (for manual entry)
+ qr_uri:
+ type: string
+ format: uri
+ description: otpauth:// URI for QR code generation
+ recovery_codes:
+ type: array
+ items:
+ type: string
+ description: 10 single-use recovery codes
+ '400':
+ description: MFA already enabled
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ /auth/mfa/verify:
+ post:
+ tags: [Auth]
+ summary: Verify TOTP code
+ description: |
+ Completes MFA setup (if pending) or verifies MFA during login.
+ On successful verification, returns a new token with `mfa: true`.
+
+ **Rate limit:** 5 requests/minute per user (prevents brute force)
+ operationId: mfaVerify
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required: [code]
+ properties:
+ code:
+ type: string
+ pattern: '^\d{6}$'
+ description: 6-digit TOTP code from authenticator app
+ responses:
+ '200':
+ description: MFA verified
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AuthTokens'
+ '401':
+ description: Invalid TOTP code
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ /invites/accept:
+ post:
+ tags: [Auth]
+ summary: Accept invitation and create account
+ description: |
+ Accepts an invite token. If the user does not exist, creates a new account.
+ If the user exists (by email), grants access to the invited project/workstream.
+
+ **Rate limit:** 10 requests/minute per IP
+ operationId: acceptInvite
+ security: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required: [token]
+ properties:
+ token:
+ type: string
+ description: Invite token from email
+ name:
+ type: string
+ description: User's full name (required if creating new account)
+ password:
+ type: string
+ format: password
+ minLength: 8
+ description: Password (required if creating new account)
+ responses:
+ '200':
+ description: Invitation accepted
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ user_id:
+ type: string
+ format: uuid
+ project_id:
+ type: string
+ format: uuid
+ role:
+ type: string
+ tokens:
+ $ref: '#/components/schemas/AuthTokens'
+ '400':
+ description: Invalid, expired, or already-used invite
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ # ============================================================================
+ # PROJECT ENDPOINTS
+ # ============================================================================
+
+ /projects:
+ get:
+ tags: [Projects]
+ summary: List all projects for the authenticated user
+ description: |
+ Returns all projects the user has access to, based on their role grants.
+
+ **RBAC:** Any authenticated user
+ **Rate limit:** 300 requests/minute per user
+ operationId: listProjects
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: stage
+ in: query
+ schema:
+ type: string
+ enum: [pre_dataroom, dataroom, closed]
+ description: Filter by deal stage
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ default: 50
+ maximum: 100
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ default: 0
+ responses:
+ '200':
+ description: List of projects
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ projects:
+ type: array
+ items:
+ $ref: '#/components/schemas/Project'
+ total:
+ type: integer
+ limit:
+ type: integer
+ offset:
+ type: integer
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+
+ post:
+ tags: [Projects]
+ summary: Create a new project
+ description: |
+ Creates a new project. The creator automatically becomes `ib_admin`.
+
+ **RBAC:** Any authenticated user (typically IB initiates deals)
+ **Rate limit:** 10 requests/minute per user
+ operationId: createProject
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProjectCreate'
+ responses:
+ '201':
+ description: Project created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Project'
+ '400':
+ description: Invalid input
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+
+ /projects/{id}:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+
+ get:
+ tags: [Projects]
+ summary: Get project details
+ description: |
+ Returns project metadata and summary statistics.
+
+ **RBAC:** Any role with access to the project
+ **Rate limit:** 300 requests/minute per user
+ operationId: getProject
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: Project details
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Project'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ patch:
+ tags: [Projects]
+ summary: Update project
+ description: |
+ Updates project metadata (name, stage, theme, etc.).
+
+ **RBAC:** `ib_admin` only
+ **Rate limit:** 60 requests/minute per user
+ operationId: updateProject
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProjectUpdate'
+ responses:
+ '200':
+ description: Project updated
+ headers:
+ ETag:
+ schema:
+ type: string
+ description: Updated version tag for concurrency control
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Project'
+ '400':
+ description: Invalid input
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '409':
+ $ref: '#/components/responses/Conflict'
+ '412':
+ $ref: '#/components/responses/PreconditionFailed'
+
+ delete:
+ tags: [Projects]
+ summary: Soft delete project
+ description: |
+ Marks the project as deleted. Data is retained for compliance but
+ project becomes inaccessible to all users.
+
+ **RBAC:** `ib_admin` only
+ **Rate limit:** 10 requests/minute per user
+ operationId: deleteProject
+ security:
+ - bearerAuth: []
+ responses:
+ '204':
+ description: Project deleted
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ # ============================================================================
+ # WORKSTREAM ENDPOINTS
+ # ============================================================================
+
+ /projects/{id}/workstreams:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+
+ get:
+ tags: [Workstreams]
+ summary: List workstreams in project
+ description: |
+ Returns all workstreams the user has access to within the project.
+
+ **RBAC:** Any role with project access (filtered by workstream-level access)
+ **Rate limit:** 300 requests/minute per user
+ operationId: listWorkstreams
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: List of workstreams
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ workstreams:
+ type: array
+ items:
+ $ref: '#/components/schemas/Workstream'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ post:
+ tags: [Workstreams]
+ summary: Create workstream
+ description: |
+ Creates a new workstream within the project. Workstreams are RBAC anchors.
+
+ **RBAC:** `ib_admin` only
+ **Rate limit:** 60 requests/minute per user
+ operationId: createWorkstream
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/WorkstreamCreate'
+ responses:
+ '201':
+ description: Workstream created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Workstream'
+ '400':
+ description: Invalid input
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ /projects/{id}/workstreams/{wsId}:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+
+ patch:
+ tags: [Workstreams]
+ summary: Update workstream
+ description: |
+ Updates workstream metadata (name, description).
+
+ **RBAC:** `ib_admin` or `ib_member` with access to this workstream
+ **Rate limit:** 60 requests/minute per user
+ operationId: updateWorkstream
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/WorkstreamUpdate'
+ responses:
+ '200':
+ description: Workstream updated
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Workstream'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ # ============================================================================
+ # REQUEST LIST ENDPOINTS
+ # ============================================================================
+
+ /projects/{id}/workstreams/{wsId}/lists:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+
+ get:
+ tags: [Request Lists]
+ summary: List request lists in workstream
+ description: |
+ Returns all request lists within the workstream.
+
+ **RBAC:** Any role with workstream access
+ **Rate limit:** 300 requests/minute per user
+ operationId: listRequestLists
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: List of request lists
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ lists:
+ type: array
+ items:
+ $ref: '#/components/schemas/RequestList'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ post:
+ tags: [Request Lists]
+ summary: Create request list
+ description: |
+ Creates a new named collection of requests within the workstream.
+
+ **RBAC:** `ib_admin`, `ib_member`
+ **Rate limit:** 60 requests/minute per user
+ operationId: createRequestList
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RequestListCreate'
+ responses:
+ '201':
+ description: Request list created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RequestList'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ # ============================================================================
+ # REQUEST ENDPOINTS
+ # ============================================================================
+
+ /projects/{id}/workstreams/{wsId}/requests:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+
+ get:
+ tags: [Requests]
+ summary: List requests in workstream
+ description: |
+ Returns requests in the workstream. Supports filtering by assignee, status, and stage.
+
+ **RBAC:** Any role with workstream access. Pre-dataroom entries invisible to buyers.
+ **Rate limit:** 300 requests/minute per user
+ operationId: listRequests
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: assignee
+ in: query
+ schema:
+ type: string
+ description: Filter by assignee. Use "me" for current user.
+ - name: status
+ in: query
+ schema:
+ type: string
+ enum: [open, assigned, answered, vetted, published, closed]
+ description: Filter by status
+ - name: stage
+ in: query
+ schema:
+ type: string
+ enum: [pre_dataroom, dataroom, closed]
+ description: Filter by deal stage
+ - name: list_id
+ in: query
+ schema:
+ type: string
+ format: uuid
+ description: Filter by request list
+ - name: priority
+ in: query
+ schema:
+ type: string
+ enum: [high, normal, low]
+ - name: overdue
+ in: query
+ schema:
+ type: boolean
+ description: Only return requests past due date
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ default: 50
+ maximum: 100
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ default: 0
+ responses:
+ '200':
+ description: List of requests
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ requests:
+ type: array
+ items:
+ $ref: '#/components/schemas/Request'
+ total:
+ type: integer
+ limit:
+ type: integer
+ offset:
+ type: integer
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ post:
+ tags: [Requests]
+ summary: Create request
+ description: |
+ Creates a new request in the workstream. IB/Seller create for seller action,
+ Buyers create via data room (routed to IB).
+
+ **RBAC:** `ib_admin`, `ib_member`, `buyer_admin`, `buyer_member`
+ **Rate limit:** 60 requests/minute per user
+ operationId: createRequest
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RequestCreate'
+ responses:
+ '201':
+ description: Request created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Request'
+ '400':
+ description: Invalid input
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ /projects/{id}/workstreams/{wsId}/requests/{reqId}:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+ - $ref: '#/components/parameters/RequestId'
+
+ get:
+ tags: [Requests]
+ summary: Get request details
+ description: |
+ Returns full request details including routing chain (visible to IB only).
+
+ **RBAC:** Any role with workstream access
+ **Rate limit:** 300 requests/minute per user
+ operationId: getRequest
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: Request details
+ headers:
+ ETag:
+ schema:
+ type: string
+ description: Version tag for concurrency control
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Request'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ patch:
+ tags: [Requests]
+ summary: Update request
+ description: |
+ Updates request metadata (title, body, priority, due_date, assigned_to, status).
+ Use `If-Match` header with ETag for optimistic concurrency control.
+
+ **RBAC:** `ib_admin`, `ib_member`, `seller_admin`, `seller_member` (own assignments)
+ **Rate limit:** 60 requests/minute per user
+ operationId: updateRequest
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: If-Match
+ in: header
+ schema:
+ type: string
+ description: ETag from previous GET for concurrency control
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RequestUpdate'
+ responses:
+ '200':
+ description: Request updated
+ headers:
+ ETag:
+ schema:
+ type: string
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Request'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '409':
+ $ref: '#/components/responses/Conflict'
+ '412':
+ $ref: '#/components/responses/PreconditionFailed'
+
+ /projects/{id}/workstreams/{wsId}/requests/{reqId}/forward:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+ - $ref: '#/components/parameters/RequestId'
+
+ post:
+ tags: [Requests]
+ summary: Forward request in routing chain
+ description: |
+ Forwards the request to another user. Creates a routing chain hop.
+ The `return_to_id` is set automatically so completion returns to the forwarder.
+
+ **RBAC:** Current assignee or `ib_admin`
+ **Rate limit:** 60 requests/minute per user
+ operationId: forwardRequest
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required: [to_user_id]
+ properties:
+ to_user_id:
+ type: string
+ format: uuid
+ description: User to forward the request to
+ message:
+ type: string
+ description: Optional message for the recipient
+ responses:
+ '200':
+ description: Request forwarded
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Request'
+ '400':
+ description: Invalid recipient
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ /projects/{id}/workstreams/{wsId}/requests/{reqId}/complete:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+ - $ref: '#/components/parameters/RequestId'
+
+ post:
+ tags: [Requests]
+ summary: Mark request as complete
+ description: |
+ Marks the request as completed by the current assignee. If `return_to_id` is set,
+ the request is automatically routed back to that user's inbox. Otherwise, it moves
+ to the final status.
+
+ **RBAC:** Current assignee only
+ **Rate limit:** 60 requests/minute per user
+ operationId: completeRequest
+ security:
+ - bearerAuth: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ description: Optional completion note
+ responses:
+ '200':
+ description: Request completed and routed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Request'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ # ============================================================================
+ # ANSWER ENDPOINTS
+ # ============================================================================
+
+ /projects/{id}/workstreams/{wsId}/answers:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+
+ post:
+ tags: [Answers]
+ summary: Create an answer
+ description: |
+ Creates a new answer in the workstream. Answers can be linked to one or more
+ requests. Initially created in `draft` status.
+
+ **RBAC:** `seller_admin`, `seller_member`, `ib_admin`, `ib_member`
+ **Rate limit:** 60 requests/minute per user
+ operationId: createAnswer
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AnswerCreate'
+ responses:
+ '201':
+ description: Answer created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Answer'
+ '400':
+ description: Invalid input
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ /projects/{id}/workstreams/{wsId}/answers/{ansId}:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+ - $ref: '#/components/parameters/AnswerId'
+
+ get:
+ tags: [Answers]
+ summary: Get answer details
+ description: |
+ Returns full answer details including linked requests and files.
+
+ **RBAC:** Depends on answer status. Draft/submitted visible to IB+seller.
+ Published visible to all with workstream access.
+ **Rate limit:** 300 requests/minute per user
+ operationId: getAnswer
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: Answer details
+ headers:
+ ETag:
+ schema:
+ type: string
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Answer'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ patch:
+ tags: [Answers]
+ summary: Update answer
+ description: |
+ Updates answer content. Only allowed in `draft` or `rejected` status.
+
+ **RBAC:** Answer creator, `seller_admin`, or `ib_admin`
+ **Rate limit:** 60 requests/minute per user
+ operationId: updateAnswer
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: If-Match
+ in: header
+ schema:
+ type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AnswerUpdate'
+ responses:
+ '200':
+ description: Answer updated
+ headers:
+ ETag:
+ schema:
+ type: string
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Answer'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '409':
+ $ref: '#/components/responses/Conflict'
+
+ /projects/{id}/workstreams/{wsId}/answers/{ansId}/submit:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+ - $ref: '#/components/parameters/AnswerId'
+
+ post:
+ tags: [Answers]
+ summary: Submit answer for vetting
+ description: |
+ Submits the answer for IB review. Transitions status from `draft` to `submitted`.
+
+ **RBAC:** Answer creator, `seller_admin`
+ **Rate limit:** 60 requests/minute per user
+ operationId: submitAnswer
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: Answer submitted for vetting
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Answer'
+ '400':
+ description: Answer not in draft status
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ /projects/{id}/workstreams/{wsId}/answers/{ansId}/approve:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+ - $ref: '#/components/parameters/AnswerId'
+
+ post:
+ tags: [Answers]
+ summary: Approve answer
+ description: |
+ IB approves the answer. Transitions status from `submitted` to `approved`.
+ Answer is now ready for publishing to data room.
+
+ **RBAC:** `ib_admin`, `ib_member` with workstream access
+ **Rate limit:** 60 requests/minute per user
+ operationId: approveAnswer
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: Answer approved
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Answer'
+ '400':
+ description: Answer not in submitted status
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ /projects/{id}/workstreams/{wsId}/answers/{ansId}/reject:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+ - $ref: '#/components/parameters/AnswerId'
+
+ post:
+ tags: [Answers]
+ summary: Reject answer
+ description: |
+ IB rejects the answer with a reason. Transitions status from `submitted` to `rejected`.
+ Seller can then edit and resubmit.
+
+ **RBAC:** `ib_admin`, `ib_member` with workstream access
+ **Rate limit:** 60 requests/minute per user
+ operationId: rejectAnswer
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required: [reason]
+ properties:
+ reason:
+ type: string
+ minLength: 1
+ description: Explanation for why the answer was rejected
+ responses:
+ '200':
+ description: Answer rejected
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Answer'
+ '400':
+ description: Answer not in submitted status or missing reason
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ /projects/{id}/workstreams/{wsId}/answers/{ansId}/publish:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+ - $ref: '#/components/parameters/AnswerId'
+
+ post:
+ tags: [Answers]
+ summary: Publish answer to data room
+ description: |
+ Publishes an approved answer to the data room. All linked requests are updated
+ and broadcast notifications sent to all requesters who asked equivalent questions.
+
+ **RBAC:** `ib_admin`, `ib_member` with workstream access
+ **Rate limit:** 60 requests/minute per user
+ operationId: publishAnswer
+ security:
+ - bearerAuth: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ broadcast_to:
+ type: string
+ enum: [linked_requesters, all_workstream, all_dataroom]
+ default: linked_requesters
+ description: |
+ Broadcast scope:
+ - `linked_requesters`: Only users who submitted linked requests
+ - `all_workstream`: All users with workstream access
+ - `all_dataroom`: All users with any data room access
+ responses:
+ '200':
+ description: Answer published
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Answer'
+ '400':
+ description: Answer not in approved status
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ # ============================================================================
+ # AI MATCHING ENDPOINTS
+ # ============================================================================
+
+ /projects/{id}/workstreams/{wsId}/requests/{reqId}/matches:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+ - $ref: '#/components/parameters/RequestId'
+
+ get:
+ tags: [Matches]
+ summary: Get AI-suggested answer matches
+ description: |
+ Returns published answers that the AI has identified as potentially satisfying
+ this request (cosine similarity >= 0.72). Human confirmation is required before
+ linking.
+
+ **RBAC:** `ib_admin`, `ib_member`
+ **Rate limit:** 20 requests/minute per user (embedding cost control)
+ operationId: getMatches
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: min_score
+ in: query
+ schema:
+ type: number
+ minimum: 0
+ maximum: 1
+ default: 0.72
+ description: Minimum similarity score threshold
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ default: 10
+ maximum: 25
+ responses:
+ '200':
+ description: List of suggested matches
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ matches:
+ type: array
+ items:
+ $ref: '#/components/schemas/AnswerMatch'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ /projects/{id}/workstreams/{wsId}/requests/{reqId}/matches/{answerId}/confirm:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+ - $ref: '#/components/parameters/RequestId'
+ - $ref: '#/components/parameters/AnswerIdPath'
+
+ post:
+ tags: [Matches]
+ summary: Confirm AI match
+ description: |
+ Human confirms that the suggested answer satisfies this request.
+ Creates a confirmed link in `answer_links` and updates request status.
+ Triggers broadcast to the requester.
+
+ **RBAC:** `ib_admin`, `ib_member`
+ **Rate limit:** 60 requests/minute per user
+ operationId: confirmMatch
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: Match confirmed
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ request:
+ $ref: '#/components/schemas/Request'
+ answer:
+ $ref: '#/components/schemas/Answer'
+ link_id:
+ type: string
+ format: uuid
+ '400':
+ description: Answer not published or already linked
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ /projects/{id}/workstreams/{wsId}/requests/{reqId}/matches/{answerId}/reject:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/WorkstreamId'
+ - $ref: '#/components/parameters/RequestId'
+ - $ref: '#/components/parameters/AnswerIdPath'
+
+ post:
+ tags: [Matches]
+ summary: Reject AI match
+ description: |
+ Human rejects the AI suggestion. The answer will not be suggested again for
+ this request (stored in `answer_links` with rejected flag).
+
+ **RBAC:** `ib_admin`, `ib_member`
+ **Rate limit:** 60 requests/minute per user
+ operationId: rejectMatch
+ security:
+ - bearerAuth: []
+ responses:
+ '200':
+ description: Match rejected
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: Match rejected and will not be suggested again
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ # ============================================================================
+ # FILE/OBJECT ENDPOINTS
+ # ============================================================================
+
+ /projects/{id}/objects:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+
+ post:
+ tags: [Objects]
+ summary: Upload file
+ description: |
+ Uploads a file to the project's object store. Files are encrypted at rest.
+ Returns an objectId that can be referenced in answers.
+
+ **RBAC:** `ib_admin`, `ib_member`, `seller_admin`, `seller_member`
+ **Rate limit:** 10 uploads/minute per user
+ **Max size:** 100MB per file
+ operationId: uploadObject
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ required: [file]
+ properties:
+ file:
+ type: string
+ format: binary
+ filename:
+ type: string
+ description: Override filename (defaults to uploaded name)
+ description:
+ type: string
+ description: File description for search
+ responses:
+ '201':
+ description: File uploaded
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ object_id:
+ type: string
+ pattern: '^[a-f0-9]{16}$'
+ description: Content-addressable object ID
+ filename:
+ type: string
+ size:
+ type: integer
+ description: File size in bytes
+ mime_type:
+ type: string
+ uploaded_at:
+ type: string
+ format: date-time
+ '400':
+ description: Invalid file or missing data
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '413':
+ description: File too large
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+
+ /projects/{id}/objects/{objectId}:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/ObjectId'
+
+ get:
+ tags: [Objects]
+ summary: Download file with watermark
+ description: |
+ Downloads a file with dynamic watermark applied. Watermarks include:
+ - User name and organization
+ - Timestamp
+ - CONFIDENTIAL marking
+
+ PDF, Word, Excel, and images have visible watermarks.
+ Other files are encrypted downloads only.
+
+ **RBAC:** Any role with access to the answer containing this file
+ **Rate limit:** 50 downloads/minute per user (exfiltration protection)
+ operationId: downloadObject
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: preview
+ in: query
+ schema:
+ type: boolean
+ default: false
+ description: Return preview (lower quality) instead of full file
+ responses:
+ '200':
+ description: File content with watermark
+ headers:
+ Content-Disposition:
+ schema:
+ type: string
+ example: 'attachment; filename="document.pdf"'
+ Content-Type:
+ schema:
+ type: string
+ X-Watermark-Applied:
+ schema:
+ type: string
+ enum: [visible, encrypted, none]
+ content:
+ '*/*':
+ schema:
+ type: string
+ format: binary
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ delete:
+ tags: [Objects]
+ summary: Delete file
+ description: |
+ Deletes a file from the object store. File must not be referenced by any
+ published answer.
+
+ **RBAC:** File uploader, `ib_admin`, `seller_admin`
+ **Rate limit:** 60 requests/minute per user
+ operationId: deleteObject
+ security:
+ - bearerAuth: []
+ responses:
+ '204':
+ description: File deleted
+ '400':
+ description: File is referenced by published answer
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ # ============================================================================
+ # TASK ENDPOINTS
+ # ============================================================================
+
+ /tasks:
+ get:
+ tags: [Tasks]
+ summary: List personal tasks
+ description: |
+ Returns all tasks (requests) assigned to the current user across all projects.
+ This is the user's personal inbox.
+
+ **RBAC:** Any authenticated user
+ **Rate limit:** 300 requests/minute per user
+ operationId: listTasks
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: status
+ in: query
+ schema:
+ type: string
+ enum: [open, assigned, answered, vetted, published, closed]
+ description: Filter by status
+ - name: overdue
+ in: query
+ schema:
+ type: boolean
+ description: Only return tasks past due date
+ - name: project_id
+ in: query
+ schema:
+ type: string
+ format: uuid
+ description: Filter by project
+ - name: priority
+ in: query
+ schema:
+ type: string
+ enum: [high, normal, low]
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ default: 50
+ maximum: 100
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ default: 0
+ responses:
+ '200':
+ description: List of tasks
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ tasks:
+ type: array
+ items:
+ $ref: '#/components/schemas/Task'
+ total:
+ type: integer
+ limit:
+ type: integer
+ offset:
+ type: integer
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+
+ # ============================================================================
+ # ACCESS ENDPOINTS
+ # ============================================================================
+
+ /projects/{id}/access:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+
+ get:
+ tags: [Access]
+ summary: List access grants
+ description: |
+ Returns all access grants for the project.
+
+ **RBAC:** `ib_admin`, `seller_admin` (own org), `buyer_admin` (own org)
+ **Rate limit:** 300 requests/minute per user
+ operationId: listAccess
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: role
+ in: query
+ schema:
+ type: string
+ enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer]
+ description: Filter by role
+ - name: workstream_id
+ in: query
+ schema:
+ type: string
+ format: uuid
+ description: Filter by workstream
+ responses:
+ '200':
+ description: List of access grants
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ grants:
+ type: array
+ items:
+ $ref: '#/components/schemas/AccessGrant'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ post:
+ tags: [Access]
+ summary: Grant access
+ description: |
+ Grants a role to a user for the project and/or specific workstream.
+ Users can only grant roles at or below their level.
+ IB can grant any role. Seller/Buyer can only grant within their org type.
+
+ **RBAC:** `ib_admin`, `seller_admin`, `buyer_admin` (with hierarchy constraints)
+ **Rate limit:** 60 requests/minute per user
+ operationId: grantAccess
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AccessGrantCreate'
+ responses:
+ '201':
+ description: Access granted
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AccessGrant'
+ '400':
+ description: Invalid input or role hierarchy violation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ /projects/{id}/access/{grantId}:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/GrantId'
+
+ delete:
+ tags: [Access]
+ summary: Revoke access
+ description: |
+ Revokes an access grant. All active sessions for the affected user are immediately
+ invalidated for this project.
+
+ **RBAC:** `ib_admin`, or admin of same org type who granted the access
+ **Rate limit:** 60 requests/minute per user
+ operationId: revokeAccess
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: reason
+ in: query
+ schema:
+ type: string
+ description: Audit trail reason for revocation
+ responses:
+ '204':
+ description: Access revoked
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ # ============================================================================
+ # INVITE ENDPOINTS
+ # ============================================================================
+
+ /projects/{id}/invites:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+
+ get:
+ tags: [Invites]
+ summary: List pending invites
+ description: |
+ Returns all pending invitations for the project.
+
+ **RBAC:** `ib_admin`, `seller_admin`, `buyer_admin`
+ **Rate limit:** 300 requests/minute per user
+ operationId: listInvites
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: status
+ in: query
+ schema:
+ type: string
+ enum: [pending, accepted, expired, revoked]
+ responses:
+ '200':
+ description: List of invites
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ invites:
+ type: array
+ items:
+ $ref: '#/components/schemas/Invite'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ post:
+ tags: [Invites]
+ summary: Create invite
+ description: |
+ Creates an invitation for a user to join the project with a specific role.
+ Invites are scoped to a project and optionally to specific workstreams.
+ Invite token expires in 72 hours.
+
+ **RBAC:** `ib_admin`, `seller_admin`, `buyer_admin` (with role hierarchy constraints)
+ **Rate limit:** 30 requests/minute per user
+ operationId: createInvite
+ security:
+ - bearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/InviteCreate'
+ responses:
+ '201':
+ description: Invite created and sent
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Invite'
+ '400':
+ description: Invalid input or role hierarchy violation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ /projects/{id}/invites/{inviteId}:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+ - $ref: '#/components/parameters/InviteId'
+
+ delete:
+ tags: [Invites]
+ summary: Revoke invite
+ description: |
+ Revokes a pending invitation. If already accepted, use access revocation instead.
+
+ **RBAC:** Invite creator or `ib_admin`
+ **Rate limit:** 60 requests/minute per user
+ operationId: revokeInvite
+ security:
+ - bearerAuth: []
+ responses:
+ '204':
+ description: Invite revoked
+ '400':
+ description: Invite already accepted
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '404':
+ $ref: '#/components/responses/NotFound'
+
+ # ============================================================================
+ # AUDIT ENDPOINT
+ # ============================================================================
+
+ /projects/{id}/audit:
+ parameters:
+ - $ref: '#/components/parameters/ProjectId'
+
+ get:
+ tags: [Audit]
+ summary: Get audit log
+ description: |
+ Returns paginated audit log entries for the project. Includes all security-relevant
+ events: access changes, file downloads, status transitions, logins.
+
+ Audit entries are hash-chained for tamper evidence.
+
+ **RBAC:** `ib_admin` only
+ **Rate limit:** 30 requests/minute per user
+ operationId: getAuditLog
+ security:
+ - bearerAuth: []
+ parameters:
+ - name: action
+ in: query
+ schema:
+ type: string
+ description: Filter by action type (e.g., "file.downloaded", "access.granted")
+ - name: actor_id
+ in: query
+ schema:
+ type: string
+ format: uuid
+ description: Filter by actor
+ - name: target_id
+ in: query
+ schema:
+ type: string
+ format: uuid
+ description: Filter by target entry/user/file
+ - name: from
+ in: query
+ schema:
+ type: string
+ format: date-time
+ description: Start of time range
+ - name: to
+ in: query
+ schema:
+ type: string
+ format: date-time
+ description: End of time range
+ - name: limit
+ in: query
+ schema:
+ type: integer
+ default: 100
+ maximum: 500
+ - name: offset
+ in: query
+ schema:
+ type: integer
+ default: 0
+ responses:
+ '200':
+ description: Audit log entries
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ entries:
+ type: array
+ items:
+ $ref: '#/components/schemas/AuditEntry'
+ total:
+ type: integer
+ limit:
+ type: integer
+ offset:
+ type: integer
+ chain_verified:
+ type: boolean
+ description: Whether hash chain integrity was verified
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+
+ # ============================================================================
+ # HEALTH ENDPOINT
+ # ============================================================================
+
+ /health:
+ get:
+ tags: [Health]
+ summary: Service health check
+ description: |
+ Returns service health status. No authentication required.
+ Suitable for load balancer and monitoring probes.
+ operationId: healthCheck
+ security: []
+ responses:
+ '200':
+ description: Service healthy
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ enum: [healthy, degraded]
+ version:
+ type: string
+ example: "1.0.0"
+ uptime:
+ type: integer
+ description: Uptime in seconds
+ checks:
+ type: object
+ properties:
+ database:
+ type: string
+ enum: [ok, error]
+ object_store:
+ type: string
+ enum: [ok, error]
+ redis:
+ type: string
+ enum: [ok, error]
+ '503':
+ description: Service unhealthy
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ enum: [unhealthy]
+ errors:
+ type: array
+ items:
+ type: string
+
+# ==============================================================================
+# COMPONENTS
+# ==============================================================================
+
+components:
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ bearerFormat: JWT
+ description: |
+ JWT token from `/auth/login` or `/auth/refresh`.
+ Token contains: sub (user_id), org, roles[], mfa, sid, fp, exp.
+
+ parameters:
+ ProjectId:
+ name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: Project ID
+
+ WorkstreamId:
+ name: wsId
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: Workstream ID
+
+ RequestId:
+ name: reqId
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: Request ID
+
+ AnswerId:
+ name: ansId
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: Answer ID
+
+ AnswerIdPath:
+ name: answerId
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: Answer ID for matching
+
+ ObjectId:
+ name: objectId
+ in: path
+ required: true
+ schema:
+ type: string
+ pattern: '^[a-f0-9]{16}$'
+ description: Content-addressable object ID
+
+ GrantId:
+ name: grantId
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: Access grant ID
+
+ InviteId:
+ name: inviteId
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: Invite ID
+
+ responses:
+ Unauthorized:
+ description: Authentication required or token invalid
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ example:
+ error: "Invalid or expired authentication token"
+ code: "UNAUTHORIZED"
+ details: {}
+
+ Forbidden:
+ description: Insufficient permissions for this operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ example:
+ error: "You do not have permission to perform this action"
+ code: "FORBIDDEN"
+ details:
+ required_role: "ib_admin"
+
+ NotFound:
+ description: Resource not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ example:
+ error: "Resource not found"
+ code: "NOT_FOUND"
+ details: {}
+
+ Conflict:
+ description: Concurrent modification conflict
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ example:
+ error: "Resource was modified by another user"
+ code: "CONFLICT"
+ details:
+ current_version: 5
+
+ PreconditionFailed:
+ description: ETag mismatch - resource changed since last fetch
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ example:
+ error: "Resource has been modified. Fetch the latest version and retry."
+ code: "PRECONDITION_FAILED"
+ details:
+ current_etag: '"abc123"'
+
+ schemas:
+ Error:
+ type: object
+ required: [error, code]
+ properties:
+ error:
+ type: string
+ description: Human-readable error message
+ code:
+ type: string
+ description: Machine-readable error code
+ enum:
+ - BAD_REQUEST
+ - UNAUTHORIZED
+ - FORBIDDEN
+ - NOT_FOUND
+ - CONFLICT
+ - PRECONDITION_FAILED
+ - RATE_LIMIT_EXCEEDED
+ - INTERNAL_ERROR
+ - INVALID_INVITE
+ - INVITE_EXPIRED
+ - INVITE_ALREADY_USED
+ - EMAIL_MISMATCH
+ - MFA_REQUIRED
+ - INVALID_TOTP
+ - CONCURRENT_MODIFICATION
+ details:
+ type: object
+ additionalProperties: true
+ description: Additional error context
+
+ AuthTokens:
+ type: object
+ properties:
+ access_token:
+ type: string
+ description: JWT access token (1 hour lifetime)
+ refresh_token:
+ type: string
+ description: Refresh token (7 day lifetime, rotates on use)
+ token_type:
+ type: string
+ enum: [Bearer]
+ expires_in:
+ type: integer
+ description: Access token lifetime in seconds
+ mfa_required:
+ type: boolean
+ description: Whether MFA verification is pending
+ user:
+ $ref: '#/components/schemas/User'
+
+ User:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ email:
+ type: string
+ format: email
+ name:
+ type: string
+ organization_id:
+ type: string
+ format: uuid
+ organization_name:
+ type: string
+ mfa_enabled:
+ type: boolean
+ created_at:
+ type: string
+ format: date-time
+
+ Project:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ name:
+ type: string
+ example: "Project Falcon"
+ description:
+ type: string
+ stage:
+ type: string
+ enum: [pre_dataroom, dataroom, closed]
+ theme_id:
+ type: string
+ format: uuid
+ nullable: true
+ created_by:
+ type: string
+ format: uuid
+ created_at:
+ type: string
+ format: date-time
+ updated_at:
+ type: string
+ format: date-time
+ stats:
+ type: object
+ properties:
+ workstreams:
+ type: integer
+ requests_total:
+ type: integer
+ requests_open:
+ type: integer
+ requests_completed:
+ type: integer
+ answers_published:
+ type: integer
+ my_role:
+ type: string
+ enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer]
+ description: Current user's role in this project
+
+ ProjectCreate:
+ type: object
+ required: [name]
+ properties:
+ name:
+ type: string
+ minLength: 1
+ maxLength: 255
+ description:
+ type: string
+ maxLength: 2000
+ theme_id:
+ type: string
+ format: uuid
+
+ ProjectUpdate:
+ type: object
+ properties:
+ name:
+ type: string
+ minLength: 1
+ maxLength: 255
+ description:
+ type: string
+ maxLength: 2000
+ stage:
+ type: string
+ enum: [pre_dataroom, dataroom, closed]
+ theme_id:
+ type: string
+ format: uuid
+ nullable: true
+
+ Workstream:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ project_id:
+ type: string
+ format: uuid
+ name:
+ type: string
+ example: "Finance"
+ slug:
+ type: string
+ example: "finance"
+ description:
+ type: string
+ created_at:
+ type: string
+ format: date-time
+ updated_at:
+ type: string
+ format: date-time
+ stats:
+ type: object
+ properties:
+ requests_total:
+ type: integer
+ requests_open:
+ type: integer
+ answers_published:
+ type: integer
+
+ WorkstreamCreate:
+ type: object
+ required: [name]
+ properties:
+ name:
+ type: string
+ minLength: 1
+ maxLength: 100
+ slug:
+ type: string
+ pattern: '^[a-z0-9-]+$'
+ maxLength: 50
+ description: URL-safe identifier (auto-generated from name if not provided)
+ description:
+ type: string
+ maxLength: 1000
+
+ WorkstreamUpdate:
+ type: object
+ properties:
+ name:
+ type: string
+ minLength: 1
+ maxLength: 100
+ description:
+ type: string
+ maxLength: 1000
+
+ RequestList:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ workstream_id:
+ type: string
+ format: uuid
+ name:
+ type: string
+ example: "Initial Due Diligence"
+ description:
+ type: string
+ created_by:
+ type: string
+ format: uuid
+ created_at:
+ type: string
+ format: date-time
+ request_count:
+ type: integer
+
+ RequestListCreate:
+ type: object
+ required: [name]
+ properties:
+ name:
+ type: string
+ minLength: 1
+ maxLength: 255
+ description:
+ type: string
+ maxLength: 1000
+
+ Request:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ workstream_id:
+ type: string
+ format: uuid
+ list_id:
+ type: string
+ format: uuid
+ nullable: true
+ ref:
+ type: string
+ example: "FIN-042"
+ description: Human-readable reference number
+ title:
+ type: string
+ example: "Provide audited financials FY2024"
+ body:
+ type: string
+ priority:
+ type: string
+ enum: [high, normal, low]
+ due_date:
+ type: string
+ format: date
+ nullable: true
+ status:
+ type: string
+ enum: [open, assigned, answered, vetted, published, closed]
+ stage:
+ type: string
+ enum: [pre_dataroom, dataroom, closed]
+ assignee_id:
+ type: string
+ format: uuid
+ nullable: true
+ description: Current assignee (who has it RIGHT NOW)
+ assignee:
+ $ref: '#/components/schemas/User'
+ return_to_id:
+ type: string
+ format: uuid
+ nullable: true
+ description: Who it returns to when completed
+ origin_id:
+ type: string
+ format: uuid
+ nullable: true
+ description: Ultimate requester who started the chain
+ created_by:
+ type: string
+ format: uuid
+ created_at:
+ type: string
+ format: date-time
+ updated_at:
+ type: string
+ format: date-time
+ routing_chain:
+ type: array
+ description: Full routing history (visible to IB only)
+ items:
+ type: object
+ properties:
+ actor_id:
+ type: string
+ format: uuid
+ action:
+ type: string
+ enum: [created, forwarded, completed]
+ timestamp:
+ type: string
+ format: date-time
+ message:
+ type: string
+ linked_answers:
+ type: array
+ items:
+ type: object
+ properties:
+ answer_id:
+ type: string
+ format: uuid
+ confirmed:
+ type: boolean
+ linked_at:
+ type: string
+ format: date-time
+
+ RequestCreate:
+ type: object
+ required: [title]
+ properties:
+ title:
+ type: string
+ minLength: 1
+ maxLength: 500
+ body:
+ type: string
+ maxLength: 10000
+ list_id:
+ type: string
+ format: uuid
+ description: Optional request list to add to
+ priority:
+ type: string
+ enum: [high, normal, low]
+ default: normal
+ due_date:
+ type: string
+ format: date
+ assigned_to:
+ type: array
+ items:
+ type: string
+ format: uuid
+ description: Initial assignees
+
+ RequestUpdate:
+ type: object
+ properties:
+ title:
+ type: string
+ minLength: 1
+ maxLength: 500
+ body:
+ type: string
+ maxLength: 10000
+ priority:
+ type: string
+ enum: [high, normal, low]
+ due_date:
+ type: string
+ format: date
+ nullable: true
+ assigned_to:
+ type: array
+ items:
+ type: string
+ format: uuid
+ status:
+ type: string
+ enum: [open, assigned, answered, vetted, published, closed]
+
+ Answer:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ workstream_id:
+ type: string
+ format: uuid
+ title:
+ type: string
+ body:
+ type: string
+ status:
+ type: string
+ enum: [draft, submitted, approved, rejected, published]
+ rejection_reason:
+ type: string
+ nullable: true
+ files:
+ type: array
+ items:
+ type: object
+ properties:
+ object_id:
+ type: string
+ pattern: '^[a-f0-9]{16}$'
+ filename:
+ type: string
+ size:
+ type: integer
+ mime_type:
+ type: string
+ linked_requests:
+ type: array
+ items:
+ type: object
+ properties:
+ request_id:
+ type: string
+ format: uuid
+ ref:
+ type: string
+ title:
+ type: string
+ confirmed:
+ type: boolean
+ broadcast_to:
+ type: string
+ enum: [linked_requesters, all_workstream, all_dataroom]
+ nullable: true
+ created_by:
+ type: string
+ format: uuid
+ created_at:
+ type: string
+ format: date-time
+ updated_at:
+ type: string
+ format: date-time
+ submitted_at:
+ type: string
+ format: date-time
+ nullable: true
+ approved_at:
+ type: string
+ format: date-time
+ nullable: true
+ published_at:
+ type: string
+ format: date-time
+ nullable: true
+
+ AnswerCreate:
+ type: object
+ required: [title]
+ properties:
+ title:
+ type: string
+ minLength: 1
+ maxLength: 500
+ body:
+ type: string
+ maxLength: 50000
+ file_ids:
+ type: array
+ items:
+ type: string
+ pattern: '^[a-f0-9]{16}$'
+ description: Object IDs of uploaded files
+ request_ids:
+ type: array
+ items:
+ type: string
+ format: uuid
+ description: Requests this answer is linked to
+
+ AnswerUpdate:
+ type: object
+ properties:
+ title:
+ type: string
+ minLength: 1
+ maxLength: 500
+ body:
+ type: string
+ maxLength: 50000
+ file_ids:
+ type: array
+ items:
+ type: string
+ pattern: '^[a-f0-9]{16}$'
+ request_ids:
+ type: array
+ items:
+ type: string
+ format: uuid
+
+ AnswerMatch:
+ type: object
+ properties:
+ answer_id:
+ type: string
+ format: uuid
+ answer_title:
+ type: string
+ answer_status:
+ type: string
+ enum: [published]
+ similarity_score:
+ type: number
+ format: float
+ minimum: 0
+ maximum: 1
+ description: Cosine similarity score (AI-computed)
+ matched_text:
+ type: string
+ description: Snippet of answer that matched the request
+ linked_requests:
+ type: integer
+ description: Number of other requests already linked to this answer
+
+ Task:
+ type: object
+ description: A request in the user's personal inbox
+ properties:
+ id:
+ type: string
+ format: uuid
+ project_id:
+ type: string
+ format: uuid
+ project_name:
+ type: string
+ workstream_id:
+ type: string
+ format: uuid
+ workstream_name:
+ type: string
+ ref:
+ type: string
+ title:
+ type: string
+ priority:
+ type: string
+ enum: [high, normal, low]
+ due_date:
+ type: string
+ format: date
+ nullable: true
+ status:
+ type: string
+ enum: [open, assigned, answered, vetted, published, closed]
+ is_overdue:
+ type: boolean
+ return_to:
+ $ref: '#/components/schemas/User'
+ forwarded_by:
+ $ref: '#/components/schemas/User'
+ assigned_at:
+ type: string
+ format: date-time
+ created_at:
+ type: string
+ format: date-time
+
+ AccessGrant:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ project_id:
+ type: string
+ format: uuid
+ workstream_id:
+ type: string
+ format: uuid
+ nullable: true
+ description: If null, access applies to all workstreams
+ user_id:
+ type: string
+ format: uuid
+ user:
+ $ref: '#/components/schemas/User'
+ role:
+ type: string
+ enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer]
+ ops:
+ type: string
+ description: "Operation flags: r=read, w=write, d=delete, m=manage"
+ example: "rw"
+ granted_by:
+ type: string
+ format: uuid
+ granted_at:
+ type: string
+ format: date-time
+
+ AccessGrantCreate:
+ type: object
+ required: [user_id, role]
+ properties:
+ user_id:
+ type: string
+ format: uuid
+ role:
+ type: string
+ enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer]
+ workstream_id:
+ type: string
+ format: uuid
+ nullable: true
+ description: Scope to specific workstream (null = all workstreams)
+ ops:
+ type: string
+ default: "rw"
+ description: "Operation flags: r=read, w=write, d=delete, m=manage"
+
+ Invite:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ project_id:
+ type: string
+ format: uuid
+ workstream_id:
+ type: string
+ format: uuid
+ nullable: true
+ email:
+ type: string
+ format: email
+ role:
+ type: string
+ enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer]
+ status:
+ type: string
+ enum: [pending, accepted, expired, revoked]
+ invited_by:
+ type: string
+ format: uuid
+ invited_by_name:
+ type: string
+ expires_at:
+ type: string
+ format: date-time
+ accepted_at:
+ type: string
+ format: date-time
+ nullable: true
+ created_at:
+ type: string
+ format: date-time
+
+ InviteCreate:
+ type: object
+ required: [email, role]
+ properties:
+ email:
+ type: string
+ format: email
+ role:
+ type: string
+ enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer]
+ workstream_id:
+ type: string
+ format: uuid
+ nullable: true
+ description: Scope to specific workstream
+ message:
+ type: string
+ maxLength: 1000
+ description: Personal message to include in invite email
+
+ AuditEntry:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ project_id:
+ type: string
+ format: uuid
+ actor_id:
+ type: string
+ format: uuid
+ actor_email:
+ type: string
+ format: email
+ action:
+ type: string
+ description: |
+ Event type. Examples:
+ - auth.login, auth.logout, auth.mfa_enabled
+ - access.granted, access.revoked
+ - entry.created, entry.updated, entry.published
+ - file.uploaded, file.downloaded, file.deleted
+ - session.created, session.revoked
+ target_type:
+ type: string
+ enum: [entry, user, access, session, file]
+ nullable: true
+ target_id:
+ type: string
+ format: uuid
+ nullable: true
+ details:
+ type: object
+ additionalProperties: true
+ description: Action-specific details
+ ip_address:
+ type: string
+ user_agent:
+ type: string
+ timestamp:
+ type: string
+ format: date-time
+ hash:
+ type: string
+ description: SHA-256 hash for tamper detection (chain integrity)
diff --git a/ONBOARDING-SPEC.md b/ONBOARDING-SPEC.md
new file mode 100644
index 0000000..60f5f0b
--- /dev/null
+++ b/ONBOARDING-SPEC.md
@@ -0,0 +1,1500 @@
+# Dealspace — Onboarding Flow Specification
+
+**Version:** 0.1 — 2026-02-28
+**Status:** Pre-implementation specification
+
+---
+
+## 1. Account Creation (IB Admin)
+
+### 1.1 Registration Form
+
+**URL:** `https://app.muskepo.com/register`
+
+**Form Fields:**
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Create Your Account │
+├─────────────────────────────────────────────────────────────┤
+│ Organization Name [Goldman Sachs ] │
+│ Your Name [Sarah Mitchell ] │
+│ Work Email [sarah.mitchell@gs.com ] │
+│ Password [•••••••••••••• ] │
+│ Confirm Password [•••••••••••••• ] │
+│ │
+│ □ I agree to the Terms of Service and Privacy Policy │
+│ │
+│ [ Create Account ] │
+│ │
+│ Already have an account? Sign in │
+└─────────────────────────────────────────────────────────────┘
+```
+
+**Validation:**
+- Email must be corporate domain (no gmail.com, yahoo.com, etc.) — soft warning, not hard block
+- Password: 12+ characters, at least one uppercase, one number, one special
+- Organization name: required, 2–100 characters
+
+**Data Created:**
+```sql
+-- User record (pending verification)
+INSERT INTO users (id, email, name, password_hash, org_name, status, created_at)
+VALUES ('usr_abc123', 'sarah.mitchell@gs.com', 'Sarah Mitchell', '$argon2id$...',
+ 'Goldman Sachs', 'pending_verification', 1709136000000);
+```
+
+### 1.2 Email Verification Flow
+
+**Email sent immediately after form submission:**
+
+```
+Subject: Verify your Dealspace account
+
+Hi Sarah,
+
+Click below to verify your email and activate your Dealspace account:
+
+[Verify Email Address]
+https://app.muskepo.com/verify?token=abc123def456
+
+This link expires in 24 hours.
+
+If you didn't create this account, you can safely ignore this email.
+
+—
+The Dealspace Team
+```
+
+**On click:**
+1. Token validated (single-use, 24h expiry)
+2. User status → `pending_mfa`
+3. Redirect to `/setup/mfa`
+
+### 1.3 MFA Setup (Mandatory for IB Admin)
+
+**URL:** `https://app.muskepo.com/setup/mfa`
+
+**Step-by-step flow:**
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Set Up Two-Factor Authentication │
+│ │
+│ Step 1 of 3: Install Authenticator App │
+│ ───────────────────────────────────────────────────────── │
+│ │
+│ Download one of these apps on your phone: │
+│ │
+│ • Google Authenticator (iOS / Android) │
+│ • Microsoft Authenticator (iOS / Android) │
+│ • 1Password, Authy, or any TOTP app │
+│ │
+│ Already have one? [Continue →] │
+└─────────────────────────────────────────────────────────────┘
+```
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Set Up Two-Factor Authentication │
+│ │
+│ Step 2 of 3: Scan QR Code │
+│ ───────────────────────────────────────────────────────── │
+│ │
+│ ┌───────────────┐ │
+│ │ [QR CODE] │ │
+│ │ │ │
+│ └───────────────┘ │
+│ │
+│ Can't scan? Enter this code manually: │
+│ JBSWY3DPEHPK3PXP (tap to copy) │
+│ │
+│ [Continue →] │
+└─────────────────────────────────────────────────────────────┘
+```
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Set Up Two-Factor Authentication │
+│ │
+│ Step 3 of 3: Verify Setup │
+│ ───────────────────────────────────────────────────────── │
+│ │
+│ Enter the 6-digit code from your authenticator app: │
+│ │
+│ [ 4 ] [ 7 ] [ 2 ] [ 9 ] [ 1 ] [ 5 ] │
+│ │
+│ [ Verify & Complete Setup ] │
+└─────────────────────────────────────────────────────────────┘
+```
+
+**On successful verification:**
+1. TOTP secret stored (encrypted) in user record
+2. Recovery codes generated (10 codes, 8 characters each)
+3. User status → `active`
+4. Show recovery codes screen:
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Save Your Recovery Codes │
+│ │
+│ Store these in a safe place. You'll need them if you │
+│ lose access to your authenticator app. │
+│ │
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ 1. XKCD-7843 6. BEEF-2947 │ │
+│ │ 2. PASS-9126 7. DORK-5182 │ │
+│ │ 3. MOON-4521 8. JAZZ-7394 │ │
+│ │ 4. TREE-8734 9. FORK-6215 │ │
+│ │ 5. WAVE-3069 10. LAMP-9847 │ │
+│ └─────────────────────────────────────────────────────┘ │
+│ │
+│ [Download as PDF] [Copy to clipboard] │
+│ │
+│ □ I have saved my recovery codes │
+│ │
+│ [ Continue to Dealspace ] │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 1.4 First Login Experience
+
+**URL:** `https://app.muskepo.com/app`
+
+User lands on an empty dashboard with a prominent call to action:
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ DEALSPACE Goldman Sachs | Sarah Mitchell ▼ │
+├─────────────────────────────────────────────────────────────────────────────┤
+│ │
+│ │
+│ ┌──────────────────────────┐ │
+│ │ 📁 │ │
+│ │ │ │
+│ │ No projects yet │ │
+│ │ │ │
+│ │ Create your first │ │
+│ │ deal to get started │ │
+│ │ │ │
+│ │ [+ Create Project] │ │
+│ │ │ │
+│ └──────────────────────────┘ │
+│ │
+│ │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 2. First Project Setup Wizard
+
+Clicking "Create Project" launches a 5-step wizard. Each step is a focused screen. Progress bar at top.
+
+### 2.1 Step 1: Project Name + Deal Type
+
+**URL:** `https://app.muskepo.com/app/projects/new/basics`
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ Create Project Step 1 of 5 ━━━○○○○ │
+├─────────────────────────────────────────────────────────────────────────────┤
+│ │
+│ Project Name │
+│ [TechCorp Acquisition - Project Phoenix ] │
+│ │
+│ Deal Type │
+│ ┌───────────────────┐ ┌───────────────────┐ │
+│ │ ○ Sell-side │ │ ○ Buy-side │ │
+│ │ M&A Advisory │ │ M&A Advisory │ │
+│ └───────────────────┘ └───────────────────┘ │
+│ ┌───────────────────┐ ┌───────────────────┐ │
+│ │ ○ Merger │ │ ○ Other │ │
+│ │ │ │ │ │
+│ └───────────────────┘ └───────────────────┘ │
+│ │
+│ Code Name (optional, shown to buyers instead of project name) │
+│ [Project Phoenix ] │
+│ │
+│ [Cancel] [Next: Workstreams] │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+**Data Created (in memory, not persisted until Step 5):**
+```json
+{
+ "name": "TechCorp Acquisition - Project Phoenix",
+ "deal_type": "sell_side",
+ "code_name": "Project Phoenix",
+ "workstreams": [],
+ "team": [],
+ "seller_contacts": []
+}
+```
+
+### 2.2 Step 2: Configure Workstreams
+
+**URL:** `https://app.muskepo.com/app/projects/new/workstreams`
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ Create Project Step 2 of 5 ━━━━○○○ │
+├─────────────────────────────────────────────────────────────────────────────┤
+│ │
+│ Configure Workstreams │
+│ │
+│ These are the areas your deal will cover. You can add, remove, or │
+│ rename them. Each workstream has its own access controls. │
+│ │
+│ ┌─────────────────────────────────────────────────────────────────┐ │
+│ │ ☑ Finance [Edit name] [Remove] │ │
+│ │ ☑ Legal [Edit name] [Remove] │ │
+│ │ ☑ IT [Edit name] [Remove] │ │
+│ │ ☑ HR [Edit name] [Remove] │ │
+│ │ ☑ Operations [Edit name] [Remove] │ │
+│ │ ☐ Tax [Edit name] [Remove] │ │
+│ │ ☐ Commercial [Edit name] [Remove] │ │
+│ │ ☐ Environmental [Edit name] [Remove] │ │
+│ └─────────────────────────────────────────────────────────────────┘ │
+│ │
+│ [+ Add Custom Workstream] │
+│ │
+│ [Back] [Next: Invite Team] │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+**Default workstreams (pre-checked):**
+- Finance
+- Legal
+- IT
+- HR
+- Operations
+
+**Additional suggestions (unchecked):**
+- Tax
+- Commercial
+- Environmental
+- Regulatory
+- Insurance
+
+**Custom workstream modal:**
+```
+┌────────────────────────────────────┐
+│ Add Workstream │
+│ │
+│ Name: [IP & Patents ] │
+│ │
+│ [Cancel] [Add Workstream] │
+└────────────────────────────────────┘
+```
+
+### 2.3 Step 3: Invite IB Team Members
+
+**URL:** `https://app.muskepo.com/app/projects/new/team`
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ Create Project Step 3 of 5 ━━━━━○○ │
+├─────────────────────────────────────────────────────────────────────────────┤
+│ │
+│ Invite Your Team │
+│ Add team members and assign them to workstreams. │
+│ │
+│ ┌─────────────────────────────────────────────────────────────────────┐ │
+│ │ Email Name Role Workstreams │ │
+│ ├─────────────────────────────────────────────────────────────────────┤ │
+│ │ mike@gs.com Mike Chen ib_member Finance, Tax │ │
+│ │ [Edit] [Remove] │ │
+│ ├─────────────────────────────────────────────────────────────────────┤ │
+│ │ jen@gs.com Jen Park ib_member Legal │ │
+│ │ [Edit] [Remove] │ │
+│ └─────────────────────────────────────────────────────────────────────┘ │
+│ │
+│ [+ Add Team Member] │
+│ │
+│ ────────────────────────────────────────────────────────────────────── │
+│ Or paste multiple emails (one per line): │
+│ ┌─────────────────────────────────────────────────────────────────────┐ │
+│ │ alex@gs.com │ │
+│ │ david@gs.com │ │
+│ │ lisa@gs.com │ │
+│ └─────────────────────────────────────────────────────────────────────┘ │
+│ [Parse & Add] │
+│ │
+│ [Back] [Next: Invite Seller] │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+**Add team member modal:**
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Add Team Member │
+│ │
+│ Email Address │
+│ [mike.chen@gs.com ] │
+│ │
+│ Full Name │
+│ [Mike Chen ] │
+│ │
+│ Role │
+│ ○ IB Admin (full access, can manage project) │
+│ ● IB Member (manage requests + vet answers in workstreams) │
+│ │
+│ Workstream Access │
+│ ☑ Finance │
+│ ☐ Legal │
+│ ☐ IT │
+│ ☐ HR │
+│ ☐ Operations │
+│ ☑ Tax │
+│ │
+│ [Cancel] [Add to Team] │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 2.4 Step 4: Invite Seller Contacts
+
+**URL:** `https://app.muskepo.com/app/projects/new/seller`
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ Create Project Step 4 of 5 ━━━━━━○ │
+├─────────────────────────────────────────────────────────────────────────────┤
+│ │
+│ Invite Seller Team │
+│ Add the sell-side company's contacts. They'll receive access invites. │
+│ │
+│ Seller Organization Name │
+│ [TechCorp Inc. ] │
+│ │
+│ ┌─────────────────────────────────────────────────────────────────────┐ │
+│ │ Email Name Title Role │ │
+│ ├─────────────────────────────────────────────────────────────────────┤ │
+│ │ john.smith@techcorp. John Smith CFO seller_ │ │
+│ │ com admin │ │
+│ │ [Edit] [Remove] │ │
+│ ├─────────────────────────────────────────────────────────────────────┤ │
+│ │ mary.johnson@techcorp Mary Johnson Controller seller_ │ │
+│ │ .com member │ │
+│ │ [Edit] [Remove] │ │
+│ └─────────────────────────────────────────────────────────────────────┘ │
+│ │
+│ [+ Add Seller Contact] │
+│ │
+│ ⓘ Seller Admin can manage their team and see all workstreams. │
+│ ⓘ Seller Members only see assigned workstreams. │
+│ │
+│ [Back] [Next: Review & Launch] │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+**Add seller contact modal:**
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Add Seller Contact │
+│ │
+│ Email Address │
+│ [john.smith@techcorp.com ] │
+│ │
+│ Full Name │
+│ [John Smith ] │
+│ │
+│ Title │
+│ [CFO ] │
+│ │
+│ Role │
+│ ● Seller Admin (manage seller team, all workstreams) │
+│ ○ Seller Member (answer requests in assigned workstreams) │
+│ │
+│ Workstream Access (for Seller Member only) │
+│ ☑ Finance │
+│ ☐ Legal │
+│ ... │
+│ │
+│ [Cancel] [Add Contact] │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 2.5 Step 5: Review & Launch
+
+**URL:** `https://app.muskepo.com/app/projects/new/review`
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ Create Project Step 5 of 5 ━━━━━━━ │
+├─────────────────────────────────────────────────────────────────────────────┤
+│ │
+│ Review & Launch │
+│ │
+│ ┌─────────────────────────────────────────────────────────────────────┐ │
+│ │ PROJECT DETAILS │ │
+│ │ Name: TechCorp Acquisition - Project Phoenix │ │
+│ │ Code Name: Project Phoenix (shown to buyers) │ │
+│ │ Type: Sell-side M&A Advisory [Edit] │ │
+│ └─────────────────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────────────────┐ │
+│ │ WORKSTREAMS (5) │ │
+│ │ Finance · Legal · IT · HR · Operations [Edit] │ │
+│ └─────────────────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────────────────┐ │
+│ │ YOUR TEAM (3 invites will be sent) │ │
+│ │ • Mike Chen (mike@gs.com) — IB Member, Finance + Tax │ │
+│ │ • Jen Park (jen@gs.com) — IB Member, Legal │ │
+│ │ • Alex Wong (alex@gs.com) — IB Member, IT [Edit] │ │
+│ └─────────────────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────────────────────┐ │
+│ │ SELLER TEAM (2 invites will be sent) │ │
+│ │ • John Smith (john.smith@techcorp.com) — Seller Admin │ │
+│ │ • Mary Johnson (mary.johnson@techcorp.com) — Seller Member │ │
+│ │ [Edit] │ │
+│ └─────────────────────────────────────────────────────────────────────┘ │
+│ │
+│ ⓘ 5 invitation emails will be sent when you launch. │
+│ │
+│ [Back] [Launch Project →] │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+### 2.6 Data Created on Launch
+
+**Database operations (single transaction):**
+
+```sql
+-- Project entry
+INSERT INTO entries (entry_id, project_id, parent_id, type, depth, search_key, summary, data, stage)
+VALUES ('prj_abc123', 'prj_abc123', '', 'project', 0, 'techcorp-acquisition',
+ 'TechCorp Acquisition - Project Phoenix',
+ PACK('{"name":"...","code_name":"...","deal_type":"sell_side","seller_org":"TechCorp Inc."}'),
+ 'pre_dataroom');
+
+-- Workstream entries (5x)
+INSERT INTO entries (entry_id, project_id, parent_id, type, depth, search_key, summary, data, stage)
+VALUES
+ ('ws_fin', 'prj_abc123', 'prj_abc123', 'workstream', 1, 'finance', 'Finance', PACK('{}'), 'pre_dataroom'),
+ ('ws_leg', 'prj_abc123', 'prj_abc123', 'workstream', 1, 'legal', 'Legal', PACK('{}'), 'pre_dataroom'),
+ ('ws_it', 'prj_abc123', 'prj_abc123', 'workstream', 1, 'it', 'IT', PACK('{}'), 'pre_dataroom'),
+ ('ws_hr', 'prj_abc123', 'prj_abc123', 'workstream', 1, 'hr', 'HR', PACK('{}'), 'pre_dataroom'),
+ ('ws_ops', 'prj_abc123', 'prj_abc123', 'workstream', 1, 'operations', 'Operations', PACK('{}'), 'pre_dataroom');
+
+-- Access records (for creating IB admin)
+INSERT INTO access (id, project_id, workstream_id, user_id, role, ops, granted_by, granted_at)
+VALUES ('acc_001', 'prj_abc123', NULL, 'usr_sarah', 'ib_admin', 'rwdm', 'usr_sarah', 1709136000000);
+
+-- Pending invitations (stored until accepted)
+INSERT INTO invitations (id, project_id, email, name, role, workstream_ids, invited_by, expires_at)
+VALUES
+ ('inv_001', 'prj_abc123', 'mike@gs.com', 'Mike Chen', 'ib_member', '["ws_fin"]', 'usr_sarah', ...),
+ ('inv_002', 'prj_abc123', 'jen@gs.com', 'Jen Park', 'ib_member', '["ws_leg"]', 'usr_sarah', ...),
+ ('inv_003', 'prj_abc123', 'john.smith@techcorp.com', 'John Smith', 'seller_admin', NULL, 'usr_sarah', ...),
+ ('inv_004', 'prj_abc123', 'mary.johnson@techcorp.com', 'Mary Johnson', 'seller_member', '["ws_fin"]', 'usr_sarah', ...);
+```
+
+### 2.7 Emails Sent on Launch
+
+**To IB team members (3x):**
+See [Section 6.1 — IB Team Invite Email](#61-ib-team-invite-email)
+
+**To Seller contacts (2x):**
+See [Section 6.2 — Seller Invite Email](#62-seller-invite-email)
+
+### 2.8 Post-Launch Screen
+
+```
+┌─────────────────────────────────────────────────────────────────────────────┐
+│ DEALSPACE [TechCorp Acquisition ▼] Goldman Sachs | Sarah ▼ │
+├─────────────────────────────────────────────────────────────────────────────┤
+│ Finance | Legal | IT | HR | Operations │
+├─────────────────────────────────────────────────────────────────────────────┤
+│ │
+│ ✓ Project Created │
+│ │
+│ 5 invitations sent. Here's what happens next: │
+│ │
+│ 1. Your team accepts their invitations │
+│ 2. Seller contacts accept and set up their accounts │
+│ 3. Create your first request list to start the due diligence │
+│ │
+│ ┌─────────────────────────────────────────────────────────────────────┐ │
+│ │ Ready to start? │ │
+│ │ │ │
+│ │ [+ Create Request List] [View Team Status] │ │
+│ └─────────────────────────────────────────────────────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 3. Invite Flow (Seller Side)
+
+### 3.1 Seller Invite Email
+
+**The actual email the Seller CFO receives:**
+
+```
+From: TechCorp Acquisition via Dealspace