Skip to content

feat: add public share function for chats - #2227

Merged
smakosh merged 7 commits into
theopenco:mainfrom
amelitoalcuitas:feat/shareable-link
May 9, 2026
Merged

smakosh merged 7 commits into
theopenco:mainfrom
amelitoalcuitas:feat/shareable-link

Conversation

@amelitoalcuitas

@amelitoalcuitas amelitoalcuitas commented May 9, 2026 •

Copy link
Copy Markdown
Contributor
image image image

Summary by CodeRabbit

New Features

  • Create and manage shareable public links for chats from the chat header.
  • View shared chats on a read-only public page.
  • Fork publicly shared chats into your own workspace with one click.
  • Chats now show share metadata (share ID and timestamp) where applicable.

@coderabbitai

coderabbitai Bot commented May 9, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 29d74166-ea8e-4174-9322-f4f548202e1e

📥 Commits

Reviewing files that changed from the base of the PR and between cb69159 and 7cde30a.

⛔ Files ignored due to path filters (3)
  • apps/code/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • ee/admin/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
📒 Files selected for processing (6)
  • apps/api/src/routes/chats.ts
  • apps/playground/src/app/share/[shareId]/page.tsx
  • apps/playground/src/components/playground/fork-chat-button.tsx
  • packages/db/migrations/1778305250_broad_hammerhead.sql
  • packages/db/migrations/meta/1778305250_snapshot.json
  • packages/db/src/schema.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/db/src/schema.ts
  • apps/playground/src/app/share/[shareId]/page.tsx
  • packages/db/migrations/1778305250_broad_hammerhead.sql
  • apps/playground/src/components/playground/fork-chat-button.tsx
  • apps/api/src/routes/chats.ts

Walkthrough

Adds DB-backed chat sharing: create/copy/delete public share links, public read-only retrieval, and fork shared snapshots into new chats; includes API, migration, Drizzle relations, frontend hooks, components, and a public share page.

Changes

Chat Sharing Feature

Layer / File(s) Summary
Database Schema
packages/db/migrations/1778305250_broad_hammerhead.sql, packages/db/src/schema.ts
Creates chat_share table with messages jsonb, created/updated timestamps, nullable deleted_at, partial unique index on active chat_id, and FK cascade rules.
Drizzle Relations & Migration Journal
packages/db/src/relations.ts, packages/db/migrations/meta/_journal.json
Adds user.chatShares, chat.shares, and chatShare relations; appends new migration journal entry.
API Zod/OpenAPI Schemas
apps/api/src/routes/public-chat-shares.ts, apps/api/src/routes/chats.ts
Introduces Zod schemas for shared messages and shared-chat payloads; extends chatSchema with nullable shareId and sharedAt.
Chat Endpoints Implementation
apps/api/src/routes/chats.ts
GET /, POST /, GET /{id}, PATCH /{id} include share metadata via LEFT JOIN excluding soft-deleted shares; adds POST /{id}/share, DELETE /{id}/share, and POST /share/{shareId}/fork.
Public Share Endpoint & Router Wiring
apps/api/src/routes/public-chat-shares.ts, apps/api/src/index.ts
Adds publicChatShares router with GET /{shareId} to return validated shared snapshots (non-deleted, underlying chat active); registers it at /public/chats/share.
Frontend Types & Hooks
apps/playground/src/hooks/useChats.ts
Chat adds shareId/sharedAt; ChatMessage adds tools. New hooks: useShareChat, useDeleteChatShare, useForkSharedChat with cache invalidation and toast handling.
Public Shared Page & Conversion
apps/playground/src/app/share/[shareId]/page.tsx
Async page fetches public snapshot (no-store), generates metadata, converts backend messages into UI format (parses legacy images/tools JSON safely), and renders read-only chat with fork option.
Read-Only Message Display
apps/playground/src/components/playground/chat-ui.tsx
Adds ReadOnlyChatMessages to render message arrays using existing AssistantMessage/UserMessage in read-only mode.
Share Dialog UI
apps/playground/src/components/playground/share-chat-dialog.tsx
Adds ShareChatDialog for creating, copying, and deleting share links; manages dialog/copy state and uses share/delete hooks.
Fork Button UI
apps/playground/src/components/playground/fork-chat-button.tsx
Adds ForkChatButton that redirects unauthenticated users to login with returnUrl, auto-forks once on fork=1, calls fork mutation, shows spinners/toasts, and navigates to the new chat.
Header Integration
apps/playground/src/components/playground/chat-header.tsx, apps/playground/src/components/playground/chat-page-client.tsx
ChatHeader gains currentChatId and shareId props and conditionally renders ShareChatDialog; chat page passes currentChatId and currentChatData.chat.shareId to header.

Sequence Diagram(s)

sequenceDiagram
  participant Owner as Chat Owner
  participant OwnerUI as Playground App
  participant API as Backend API
  participant ShareDB as Database
  participant PublicUI as Shared Page
  participant PublicClient as Public Viewer

  Owner->>OwnerUI: Click share icon
  OwnerUI->>API: POST /chats/{id}/share
  API->>ShareDB: Insert chat_share snapshot
  ShareDB-->>API: share id
  API-->>OwnerUI: share url
  OwnerUI->>OwnerUI: Copy link

  PublicClient->>PublicUI: Visit shared link
  PublicUI->>API: GET /public/chats/share/{shareId}
  API->>ShareDB: Query chat_share (not deleted)
  ShareDB-->>API: messages jsonb + metadata
  API-->>PublicUI: { id, messages[] }
  PublicUI->>PublicUI: Render read-only view

  PublicClient->>PublicUI: Click fork button
  PublicUI->>API: POST /share/{shareId}/fork
  API->>ShareDB: Read & parse messages
  API->>API: Create new chat + insert messages
  API-->>PublicUI: new chat id
  PublicUI->>PublicClient: Navigate to new chat
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • smakosh
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature being introduced: public share functionality for chats.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
apps/playground/src/app/share/[shareId]/page.tsx (1)

55-64: 💤 Low value

notFound() swallows 5xx and network errors as 404 — consider distinguishing.

A flaky upstream (5xx, timeouts) currently renders the same "not found" page as a missing share, which masks real outages from monitoring and from the user. For non-404 responses, throwing (so Next's error boundary kicks in) is more accurate.

-	if (!response.ok) {
-		notFound();
-	}
+	if (response.status === 404) {
+		notFound();
+	}
+	if (!response.ok) {
+		throw new Error(`Failed to load shared chat (${response.status})`);
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/playground/src/app/share/`[shareId]/page.tsx around lines 55 - 64, The
current fetch of `${config.apiBackendUrl}/public/chats/share/${shareId}` treats
any non-ok response the same by calling notFound(), which hides 5xx and network
errors; change the logic so only a 404 calls notFound(), and for other non-OK
statuses or fetch/network failures re-throw an Error (or let the exception
bubble) so Next's error boundary handles real outages—update the fetch/response
handling around the fetch invocation and the response.ok check (and any
surrounding try/catch) to distinguish response.status === 404 vs other statuses
and to propagate errors for 5xx/timeouts instead of calling notFound().
apps/api/src/routes/chats.ts (1)

706-706: 💤 Low value

Surface a clean 4xx instead of a 500 if a stored snapshot fails schema validation.

sharedMessageSnapshotSchema.parse(share.messages) will throw a raw ZodError on legacy/malformed snapshots, which Hono's global handler turns into a 500. Either use safeParse and return a 400/410 with a clear message, or relax the parser to be tolerant (z.array(...).catch([])) so older snapshots remain forkable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/chats.ts` at line 706,
sharedMessageSnapshotSchema.parse(share.messages) can throw a ZodError and
surface as a 500; replace this with a safeParse check on share.messages (using
sharedMessageSnapshotSchema.safeParse) and handle the failure by returning a
clear 4xx response (e.g., 400 for malformed or 410 for stale/legacy snapshot)
with an explanatory message, leaving the success branch to assign messages from
parseResult.data; alternatively, if you prefer backward compatibility, relax
sharedMessageSnapshotSchema to tolerate missing/legacy shapes (e.g., make the
array schema catch([])) so parse no longer throws—apply the change where
messages is assigned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/routes/chats.ts`:
- Around line 661-738: The forkSharedChat handler currently inserts the new chat
(db.insert into tables.chat) and then inserts messages in a separate statement,
which can leave an orphan chat if the message insert fails; wrap both inserts in
a single database transaction (use the DB client's transaction API) so the chat
and its messages are committed atomically: begin a transaction, perform the
insert into tables.chat to get newChat, insert the related messages into
tables.message (using newChat.id), then commit; if any step fails roll back the
transaction and surface the error (so the chatCount check and FREE_LIMIT_REACHED
logic remain correct).

In `@apps/playground/src/app/share/`[shareId]/page.tsx:
- Around line 99-155: The toUiMessage function uses untyped casts—replace parts:
any[] with parts: UIMessage["parts"], change parsedTools =
JSON.parse(message.tools) as any[] to parse and validate into the exact part
union type (e.g., const parsedTools = JSON.parse(message.tools) as unknown and
then assert/validate it is UIMessage["parts"] or an array of the discriminated
union before pushing), and remove the final "as UIMessage" cast by returning the
object using TypeScript's "satisfies UIMessage" to preserve exact types; update
references in toUiMessage (parts, parsedTools, and the return value) accordingly
and keep the same runtime shape checks for images/tools parsing to avoid unsafe
any casts.

In `@apps/playground/src/components/playground/fork-chat-button.tsx`:
- Around line 46-58: The effect in the component can re-fire after back/refresh
because didAutoForkRef resets on remount; before calling fork() (the mutation)
remove the fork=1 query param from the URL so subsequent mounts won't see
searchParams.get("fork") === "1". Update the import from next/navigation to
include usePathname, derive the current pathname via usePathname(), construct a
new URL/searchParams without the fork key (or push the pathname alone) and
update the history (replace or push) to strip fork=1, then set
didAutoForkRef.current = true and call fork(); keep checks in the useEffect
(isLoading, user, didAutoForkRef) as-is to avoid double-triggering.

---

Nitpick comments:
In `@apps/api/src/routes/chats.ts`:
- Line 706: sharedMessageSnapshotSchema.parse(share.messages) can throw a
ZodError and surface as a 500; replace this with a safeParse check on
share.messages (using sharedMessageSnapshotSchema.safeParse) and handle the
failure by returning a clear 4xx response (e.g., 400 for malformed or 410 for
stale/legacy snapshot) with an explanatory message, leaving the success branch
to assign messages from parseResult.data; alternatively, if you prefer backward
compatibility, relax sharedMessageSnapshotSchema to tolerate missing/legacy
shapes (e.g., make the array schema catch([])) so parse no longer throws—apply
the change where messages is assigned.

In `@apps/playground/src/app/share/`[shareId]/page.tsx:
- Around line 55-64: The current fetch of
`${config.apiBackendUrl}/public/chats/share/${shareId}` treats any non-ok
response the same by calling notFound(), which hides 5xx and network errors;
change the logic so only a 404 calls notFound(), and for other non-OK statuses
or fetch/network failures re-throw an Error (or let the exception bubble) so
Next's error boundary handles real outages—update the fetch/response handling
around the fetch invocation and the response.ok check (and any surrounding
try/catch) to distinguish response.status === 404 vs other statuses and to
propagate errors for 5xx/timeouts instead of calling notFound().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5b53ade1-59ea-4183-9d4c-a29ad14366f6

📥 Commits

Reviewing files that changed from the base of the PR and between a5bbeb4 and cb69159.

⛔ Files ignored due to path filters (1)
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
📒 Files selected for processing (15)
  • apps/api/src/index.ts
  • apps/api/src/routes/chats.ts
  • apps/api/src/routes/public-chat-shares.ts
  • apps/playground/src/app/share/[shareId]/page.tsx
  • apps/playground/src/components/playground/chat-header.tsx
  • apps/playground/src/components/playground/chat-page-client.tsx
  • apps/playground/src/components/playground/chat-ui.tsx
  • apps/playground/src/components/playground/fork-chat-button.tsx
  • apps/playground/src/components/playground/share-chat-dialog.tsx
  • apps/playground/src/hooks/useChats.ts
  • packages/db/migrations/1778305250_broad_hammerhead.sql
  • packages/db/migrations/meta/1778305250_snapshot.json
  • packages/db/migrations/meta/_journal.json
  • packages/db/src/relations.ts
  • packages/db/src/schema.ts

Comment thread apps/api/src/routes/chats.ts
Comment thread apps/api/src/routes/chats.ts
Comment thread apps/playground/src/app/share/[shareId]/page.tsx
Comment thread apps/playground/src/components/playground/fork-chat-button.tsx Outdated
@smakosh
smakosh enabled auto-merge May 9, 2026 20:45
@smakosh
smakosh added this pull request to the merge queue May 9, 2026
Merged via the queue into theopenco:main with commit c88bc8f May 9, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants