Share flow
Guest-shareable conversations with an approval step.
Vocabulary
- Share request — a member asks to make a conversation externally shareable
- Share token — an opaque URL fragment granting read + follow-up chat access
- Public share page —
/share/<token>— no auth required
Flow
1. Member viewing a thread
│
▼ clicks Share
2. Share request created (pending)
│
▼ admin gets notification
3. Admin approves / rejects
│
approved
│
▼
4. Share token generated + link emailed/copied
│
▼
5. Guest opens /share/<token>
- Sees conversation snapshot
- Can ask follow-ups limited to same project context
UI patterns
Share button
Lives in:
- Project detail topbar (icon-only
<IconShare>) - Thread topbar in chat view
- Conversation ellipsis menu
Clicking opens the Share modal — NOT the grant access modal. (These were confused in an early iteration, leading to issue #5 where the share button did nothing.)
Share modal (request side)
Request share access for "<thread title>"
┌────────────────────────────────────────┐
│ This thread is private. Requesting │
│ share access will notify an admin. │
│ │
│ [optional] Message to admin: │
│ ┌────────────────────────────────┐ │
│ │ │ │
│ └────────────────────────────────┘ │
│ │
│ [Cancel] [Request] │
└────────────────────────────────────────┘
Admin review (at /admin/share-requests)
Pending requests list with:
- Requester avatar + name
- Conversation title + project
- Message (if provided)
- Approve / Reject buttons
- Optional response message
Approve → creates ShareToken row, emails requester the URL. Reject → marks request rejected, notifies requester with optional message.
Public share page (at /share/<token>)
Full conversation rendered in read-only mode. Separate topbar (not the main app sidebar) — h-[52px], shows "Shared by <name>" + project badge + Back link.
Below the conversation, a simplified composer lets the guest ask follow-up questions using the same project's documents (but can't create new conversations in the project, can't edit, can't see other conversations).
Backend
| Endpoint | Purpose |
|---|---|
| POST /share-requests | Member creates request |
| GET /share-requests?status=pending | Admin lists (with status filter) |
| POST /share-requests/:id/approve | Admin approves → creates token |
| POST /share-requests/:id/reject | Admin rejects |
| GET /share/:token | Guest reads conversation (public) |
| POST /share/:token/messages | Guest follow-up message (public) |
| DELETE /share-tokens/:id | Admin revokes (invalidates link) |
Schema
model ShareRequest {
id String @id @default(uuid())
projectId String
conversationId String
requesterId String
message String?
status ShareStatus @default(pending)
respondedAt DateTime?
respondedById String?
responseMsg String?
createdAt DateTime @default(now())
approvedTokenId String? @unique // when approved, links to the token
}
model ShareToken {
id String @id @default(uuid())
token String @unique // opaque URL fragment, random 32 bytes
projectId String
conversationId String
createdById String // approver
expiresAt DateTime?
revokedAt DateTime?
createdAt DateTime @default(now())
}
Security notes
- Token is random 32 bytes, base64url-encoded (~43 chars). Not guessable.
- Token is stored plaintext in DB — if DB leaks, all tokens leak. Acceptable for dev phase.
- No expiry by default. Add
expiresAtif needed; UI doesn't expose this yet. - Revocation is soft (
revokedAttimestamp). Checked on every guest request. - Guest follow-ups use the same RAG context as the original conversation. No leak of other conversations in the project.
UI state in the app
Notification badge on the "Share requests" nav item when pending count > 0:
{ href: "/admin/share-requests", label: "Share requests", icon: IconShare, badge: 2 }
The badge: 2 is currently hardcoded — when the backend wire-up is finished, replace with real pending count from useShareRequests({ status: "pending" }).
Related
- Access control UI — the sibling pattern for granting direct project access
- Grant access modal