Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Added chat duplication to create copies of existing chats. [#888](https://github.com/sourcebot-dev/sourcebot/pull/888)
- Added Open Graph metadata and image generation for shared chat links. [#888](https://github.com/sourcebot-dev/sourcebot/pull/888)
- [EE] Added chat sharing with specific users, allowing chat owners to invite org members to view private chats. [#888](https://github.com/sourcebot-dev/sourcebot/pull/888)

### Changed
- Changed chat permissions model from read-only flag to ownership-based access control. [#888](https://github.com/sourcebot-dev/sourcebot/pull/888)
- Improved anonymous chat experience: anonymous users can now create chats and claim them upon signing in. [#888](https://github.com/sourcebot-dev/sourcebot/pull/888)

## [4.10.30] - 2026-02-12

### Added
Expand Down
152 changes: 152 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ To build a specific package:
yarn workspace @sourcebot/<package-name> build
```

## File Naming

Files should use camelCase starting with a lowercase letter:

```
// Correct
shareChatPopover.tsx
userAvatar.tsx
apiClient.ts

// Incorrect
ShareChatPopover.tsx
UserAvatar.tsx
share-chat-popover.tsx
```
Comment thread
brendan-kellam marked this conversation as resolved.

Exceptions:
- Special files like `README.md`, `CHANGELOG.md`, `LICENSE`
- Next.js conventions: `page.tsx`, `layout.tsx`, `loading.tsx`, etc.

## Tailwind CSS

Use Tailwind color classes directly instead of CSS variable syntax:
Expand All @@ -30,6 +50,138 @@ className="border-border bg-card text-foreground text-muted-foreground bg-muted
className="border-[var(--border)] bg-[var(--card)] text-[var(--foreground)]"
```

## API Route Handlers

Route handlers should validate inputs using Zod schemas.

**Query parameters** (GET requests):

```ts
import { queryParamsSchemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
import { z } from "zod";

const myQueryParamsSchema = z.object({
q: z.string().default(''),
page: z.coerce.number().int().positive().default(1),
});

export const GET = apiHandler(async (request: NextRequest) => {
const rawParams = Object.fromEntries(
Object.keys(myQueryParamsSchema.shape).map(key => [
key,
request.nextUrl.searchParams.get(key) ?? undefined
])
);
const parsed = myQueryParamsSchema.safeParse(rawParams);

if (!parsed.success) {
return serviceErrorResponse(
queryParamsSchemaValidationError(parsed.error)
);
}

const { q, page } = parsed.data;
// ... rest of handler
});
```

**Request body** (POST/PUT/PATCH requests):

```ts
import { requestBodySchemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
import { z } from "zod";

const myRequestBodySchema = z.object({
name: z.string(),
count: z.number().optional(),
});

export const POST = apiHandler(async (request: NextRequest) => {
const body = await request.json();
const parsed = myRequestBodySchema.safeParse(body);

if (!parsed.success) {
return serviceErrorResponse(
requestBodySchemaValidationError(parsed.error)
);
}

const { name, count } = parsed.data;
// ... rest of handler
});
```

## Data Fetching

For GET requests, prefer using API routes with react-query over server actions. This provides caching benefits and better control over data refetching.

```tsx
// Preferred: API route + react-query
import { useQuery } from "@tanstack/react-query";

const { data, isLoading } = useQuery({
queryKey: ["items", id],
queryFn: () => fetch(`/api/items/${id}`).then(res => res.json()),
});
```

Server actions should be used for mutations (POST/PUT/DELETE operations), not for data fetching.

## Authentication

Use `withAuthV2` or `withOptionalAuthV2` from `@/withAuthV2` to protect server actions and API routes.

- **`withAuthV2`** - Requires authentication. Returns `notAuthenticated()` if user is not logged in.
- **`withOptionalAuthV2`** - Allows anonymous access if the org has anonymous access enabled. `user` may be `undefined`.
- **`withMinimumOrgRole`** - Wrap inside auth context to require a minimum role (e.g., `OrgRole.OWNER`).

**Important:** Always use the `prisma` instance provided by the auth context. This instance has `userScopedPrismaClientExtension` applied, which enforces repository visibility rules (e.g., filtering repos based on user permissions). Do NOT import `prisma` directly from `@/prisma` in actions or routes that return data to the client.

**Server actions** - Wrap with `sew()` for error handling:

```ts
'use server';

import { sew } from "@/actions";
import { withAuthV2 } from "@/withAuthV2";

export const myProtectedAction = async ({ id }: { id: string }) => sew(() =>
withAuthV2(async ({ org, user, prisma }) => {
// user is guaranteed to be defined
// prisma is scoped to the user
return { success: true };
})
);

export const myPublicAction = async ({ id }: { id: string }) => sew(() =>
withOptionalAuthV2(async ({ org, user, prisma }) => {
// user may be undefined for anonymous access
return { success: true };
})
);
```

**API routes** - Check `isServiceError` and return `serviceErrorResponse`:

```ts
import { serviceErrorResponse } from "@/lib/serviceError";
import { isServiceError } from "@/lib/utils";
import { withAuthV2 } from "@/withAuthV2";

export const GET = apiHandler(async (request: NextRequest) => {
const result = await withAuthV2(async ({ org, user, prisma }) => {
// ... your logic
return data;
});

if (isServiceError(result)) {
return serviceErrorResponse(result);
}

return Response.json(result);
});
```

## Branches and Pull Requests

When creating a branch or opening a PR, ask the user for:
Expand Down
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"group": "Ask Sourcebot",
"pages": [
"docs/features/ask/overview",
"docs/features/ask/chat-sharing",
"docs/features/ask/add-model-providers"
]
},
Expand Down
66 changes: 66 additions & 0 deletions docs/docs/features/ask/chat-sharing.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Chat Sharing
---

Chat sharing allows you to share Ask Sourcebot conversations with others. Whether you want to share insights with your team or make a conversation publicly accessible, Sourcebot provides flexible sharing options.

<Frame>
<img src="/images/chat-sharing-dialog.png" alt="Chat sharing dialog" />
</Frame>

## Visibility Options

Every chat has a visibility setting that controls who can access it:

<Frame>
<img src="/images/chat-visibility-dropdown.png" alt="Chat visibility dropdown" />
</Frame>

### Private (Default)
- Only the chat owner can view the conversation
- Other users cannot access the chat, even with the link
- You can explicitly invite specific org members to view the chat (requires [Enterprise license](/docs/license-key))

### Public
- Anyone with the link can view the conversation
- If [anonymous access](/docs/configuration/auth/access-settings) is enabled, no authentication is required to view
- Useful for sharing insights with your team or creating reference documentation
- Public chats can be duplicated by others

## Sharing with Specific Users

<Note>Sharing with specific users requires an [Enterprise license](/docs/license-key).</Note>

<Frame>
<img src="/images/chat-invite-users.png" alt="Invite users dialog" />
</Frame>

Chat owners can invite specific organization members to view their private chats:

1. Open the chat you want to share
2. Click the **Share** button in the top-right corner
3. Search for users by name or email
4. Select the users you want to invite
5. Click **Invite** to grant them access

Invited users will be able to:
- View the full conversation
- See all messages and code citations
- Navigate through referenced code snippets

Invited users **cannot**:
- Edit or delete the chat
- Send new messages
- Invite other users

### Removing Access

<Frame>
<img src="/images/chat-remove-user.png" alt="Remove user from chat" />
</Frame>

To remove a user's access to your chat:

1. Open the Share dialog
2. Find the user in the "People with access" list
3. Click the **X** button next to their name
Binary file added docs/images/chat-invite-users.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/chat-remove-user.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/chat-sharing-dialog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/chat-visibility-dropdown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
Warnings:

- You are about to drop the column `isReadonly` on the `Chat` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Chat" DROP COLUMN "isReadonly";
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Chat" ADD COLUMN "anonymousCreatorId" TEXT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "ChatAccess" (
"id" TEXT NOT NULL,
"chatId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "ChatAccess_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "ChatAccess_chatId_userId_key" ON "ChatAccess"("chatId", "userId");

-- AddForeignKey
ALTER TABLE "ChatAccess" ADD CONSTRAINT "ChatAccess_chatId_fkey" FOREIGN KEY ("chatId") REFERENCES "Chat"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ChatAccess" ADD CONSTRAINT "ChatAccess_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
25 changes: 22 additions & 3 deletions packages/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ model User {
apiKeys ApiKey[]

chats Chat[]
sharedChats ChatAccess[]

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Expand Down Expand Up @@ -444,8 +445,9 @@ model Chat {

name String?

createdBy User? @relation(fields: [createdById], references: [id], onDelete: Cascade)
createdById String?
createdBy User? @relation(fields: [createdById], references: [id], onDelete: Cascade)
createdById String?
anonymousCreatorId String? // For anonymous users, stores a session ID from a cookie

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Expand All @@ -454,7 +456,24 @@ model Chat {
orgId Int

visibility ChatVisibility @default(PRIVATE)
isReadonly Boolean @default(false)

messages Json // This is a JSON array of `Message` types from @ai-sdk/ui-utils.

sharedWith ChatAccess[]
}

/// Represents a user's access to a chat that has been shared with them.
/// Unlike Invite, this is not temporary or redeemable - it grants ongoing access.
model ChatAccess {
id String @id @default(cuid())

chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
chatId String

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String

createdAt DateTime @default(now())

@@unique([chatId, userId])
}
2 changes: 2 additions & 0 deletions packages/db/tools/scriptRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PrismaClient } from "@sourcebot/db";
import { ArgumentParser } from "argparse";
import { migrateDuplicateConnections } from "./scripts/migrate-duplicate-connections";
import { injectAuditData } from "./scripts/inject-audit-data";
import { injectUserData } from "./scripts/inject-user-data";
import { confirmAction } from "./utils";
import { injectRepoData } from "./scripts/inject-repo-data";
import { testRepoQueryPerf } from "./scripts/test-repo-query-perf";
Expand All @@ -13,6 +14,7 @@ export interface Script {
export const scripts: Record<string, Script> = {
"migrate-duplicate-connections": migrateDuplicateConnections,
"inject-audit-data": injectAuditData,
"inject-user-data": injectUserData,
"inject-repo-data": injectRepoData,
"test-repo-query-perf": testRepoQueryPerf,
}
Expand Down
Loading