Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ SOURCEBOT_TELEMETRY_DISABLED=true # Disables telemetry collection
NODE_ENV=development

DEBUG_WRITE_CHAT_MESSAGES_TO_FILE=true

SOURCEBOT_LIGHTHOUSE_URL=http://localhost:3003
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Added progress bar when navigating between pages. [#1204](https://github.com/sourcebot-dev/sourcebot/pull/1204)

### Changed
- Redesigned the app layout with a new collapsible sidebar navigation, replacing the previous top navigation bar. [#1097](https://github.com/sourcebot-dev/sourcebot/pull/1097)
- Expired offline license keys no longer crash the process. An expired key now degrades to the unlicensed state. [#1109](https://github.com/sourcebot-dev/sourcebot/pull/1109)

## [4.17.2] - 2026-05-16

### Added
Expand Down
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ if (condition) doSomething();
- Events fired from multiple sources (web app, MCP server, API) must NOT use the `wa_` prefix (e.g., `ask_message_sent`, `tool_used`).
- Multi-source events should include a `source` property to identify the origin (e.g., `'sourcebot-web-client'`, `'sourcebot-mcp-server'`, `'sourcebot-ask-agent'`).

## Conditional ClassNames

Use `cn()` from `@/lib/utils` for conditional classNames instead of template literal interpolation:

```tsx
// Correct
className={cn("border-b transition-colors", isActive ? "border-foreground" : "border-transparent")}

// Incorrect
className={`border-b transition-colors ${isActive ? "border-foreground" : "border-transparent"}`}
```

## Tailwind CSS

Use Tailwind color classes directly instead of CSS variable syntax:
Expand Down
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ services:
volumes:
- ./config.json:/data/config.json
- sourcebot_data:/data
env_file:
- path: .env
required: false
environment:
- CONFIG_PATH=/data/config.json
- AUTH_URL=${AUTH_URL:-http://localhost:3000}
- AUTH_SECRET=${AUTH_SECRET:-000000000000000000000000000000000} # CHANGEME: generate via `openssl rand -base64 33`
- SOURCEBOT_ENCRYPTION_KEY=${SOURCEBOT_ENCRYPTION_KEY:-000000000000000000000000000000000} # CHANGEME: generate via `openssl rand -base64 24`
- DATABASE_URL=${DATABASE_URL:-postgresql://postgres:postgres@postgres:5432/postgres} # CHANGEME
- REDIS_URL=${REDIS_URL:-redis://redis:6379} # CHANGEME
- SOURCEBOT_EE_LICENSE_KEY=${SOURCEBOT_EE_LICENSE_KEY:-}

# For the full list of environment variables see:
# https://docs.sourcebot.dev/docs/configuration/environment-variables
Expand Down
3 changes: 1 addition & 2 deletions docs/api-reference/sourcebot-public.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1147,8 +1147,7 @@
"type": "string",
"enum": [
"OWNER",
"MEMBER",
"GUEST"
"MEMBER"
]
},
"createdAt": {
Expand Down
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
]
},
"docs/license-key",
"docs/billing",
"docs/configuration/transactional-emails",
"docs/configuration/structured-logging",
"docs/configuration/audit-logs"
Expand Down
38 changes: 38 additions & 0 deletions docs/docs/billing.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: Billing
sidebarTitle: Billing
---

Sourcebot Enterprise is available on monthly and yearly plans. Both are seat-based. This page explains how seats are billed, how changes mid-term are handled, and what happens at renewal.

## Seat count
Your seat count is the number of active users in your Sourcebot instance. Seat usage is reported to Sourcebot on a daily interval, and your subscription quantity is kept in sync with that number.

## Monthly plans
Monthly subscriptions are billed at the start of each billing cycle. Users added mid-cycle are prorated across the remaining days and appear on your next invoice. Users removed mid-cycle take effect at the next cycle. There is no refund for the remainder of the current one.

In short: you can scale up at any time and pay the prorated difference. Scaling down is effectively free until the cycle rolls over.

## Yearly plans

Yearly subscriptions are billed upfront for a committed seat count. As users are added during the term, the seat count rises but you aren't charged immediately. Every three months we reconcile. Any seats added that quarter are billed, prorated across the quarters remaining in the term.

Seats only move upward during the term. Shrinking the user count does not refund, and does not reduce the seat count until renewal. At renewal, you're invoiced at your current seat count, and that number becomes the committed baseline for the next year.

### Example

Suppose you start a yearly plan in January with 100 seats.

- In Q1, your user count grows to 110. At the end of Q1, you're invoiced for 10 seats prorated across the 3 remaining quarters.
- In Q2, your user count stays at 110. No reconciliation invoice is generated.
- In Q3, your user count grows to 120. At the end of Q3, you're invoiced for 10 seats prorated across the 1 remaining quarter.
- In Q4, reconciliation does not generate a charge (there are no remaining quarters to prorate across).
- At renewal in January, you're invoiced at 120 seats for the next year. 120 becomes the new committed baseline.

## Cancellation

Cancelling a subscription takes effect at the end of the current billing cycle (monthly) or term (yearly). You retain access to Sourcebot Enterprise features until that point.

## Questions?

For billing questions, [contact us](mailto:support@sourcebot.dev).
109 changes: 59 additions & 50 deletions docs/docs/deployment/docker-compose.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,68 @@
title: "Docker Compose"
---

This guide will walk you through deploying Sourcebot locally or on a VM using Docker Compose. We will use the [docker-compose.yml](https://github.com/sourcebot-dev/sourcebot/blob/main/docker-compose.yml) file from the [Sourcebot repository](https://github.com/sourcebot-dev/sourcebot). This is the simplest way to get started with Sourcebot.
This guide will walk you through deploying Sourcebot locally or on a VM using [Docker Compose](https://github.com/sourcebot-dev/sourcebot/blob/main/docker-compose.yml). This is the simplest way to get started with Sourcebot.

If you are looking to deploy onto Kubernetes, see the [Kubernetes (Helm)](/docs/deployment/k8s) guide.

## Get started

<Steps>
<Step title="Requirements">
- docker & docker compose. Use [Docker Desktop](https://www.docker.com/products/docker-desktop/) on Mac or Windows.
</Step>
<Step title="Obtain the Docker Compose file">
Download the [docker-compose.yml](https://github.com/sourcebot-dev/sourcebot/blob/main/docker-compose.yml) file from the Sourcebot repository.

```bash wrap icon="terminal"
curl -o docker-compose.yml https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/docker-compose.yml
```
</Step>

<Step title="Create a config.json">

In the same directory as the `docker-compose.yml` file, create a [configuration file](/docs/configuration/config-file). The configuration file is a JSON file that configures Sourcebot's behaviour, including what repositories to index, language model providers, auth providers, and more.

```bash wrap icon="terminal" Create example config
touch config.json
echo '{
"$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json",
// Comments are supported.
// This config creates a single connection to GitHub.com that
// indexes the Sourcebot repository
"connections": {
"starter-connection": {
"type": "github",
"repos": [
"sourcebot-dev/sourcebot"
]
}
}
}' > config.json
```
</Step>

<Step title="Launch your instance">
Update the secrets in the `docker-compose.yml` and then run Sourcebot using:

```bash wrap icon="terminal"
docker compose up
```
</Step>

<Step title="Done">
You're all set! Navigate to [http://localhost:3000](http://localhost:3000) to access your Sourcebot instance.
</Step>
</Steps>
## System requirements
- RAM: Ensure your environment has at least 4GB of RAM. Insufficient memory can cause processes to crash.
- Docker & Docker Compose: Make sure both are installed and up-to-date.
- Node.js 18+: Required for the setup CLI

## Option 1: Setup CLI

The setup CLI will guide you through configuring your Sourcebot instance to connect to your code hosts and LLM providers. From a empty folder, run the following command:

```
npx setup-sourcebot
```

<Frame>
<img src="/images/setup_sourcebot_splash.png" alt="npx setup-sourcebot" />
</Frame>

## Option 2: Manual steps

### Obtain the Docker Compose file

Download the [docker-compose.yml](https://github.com/sourcebot-dev/sourcebot/blob/main/docker-compose.yml) file from the Sourcebot repository.

```bash wrap icon="terminal"
curl -o docker-compose.yml https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/docker-compose.yml
```

### Create a config.json

In the same directory as the `docker-compose.yml` file, create a [configuration file](/docs/configuration/config-file). The configuration file is a JSON file that configures Sourcebot's behaviour, including what repositories to index, language model providers, auth providers, and more.

```bash wrap icon="terminal" Create example config
touch config.json
echo '{
"$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json",
// Comments are supported.
// This config creates a single connection to GitHub.com that
// indexes the Sourcebot repository
"connections": {
"starter-connection": {
"type": "github",
"repos": [
"sourcebot-dev/sourcebot"
]
}
}
}' > config.json
```

### Launch your instance

Update the secrets in the `docker-compose.yml` and then run Sourcebot using:

```bash wrap icon="terminal"
docker compose up
```

Navigate to [http://localhost:3000](http://localhost:3000) to access your Sourcebot instance.

## Next steps

Expand Down
4 changes: 3 additions & 1 deletion docs/docs/license-key.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ docker run \

## Questions?

If you have any questions regarding licensing, please [contact us](https://www.sourcebot.dev/contact).
For how seats are priced and reconciled across billing cycles, see [Billing](/docs/billing).

For any other licensing questions, please [contact us](https://www.sourcebot.dev/contact).
Binary file added docs/images/setup_sourcebot_splash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions packages/backend/src/__mocks__/prisma.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { vi } from 'vitest';

export const prisma = {
license: {
findUnique: vi.fn().mockResolvedValue(null),
},
};
5 changes: 3 additions & 2 deletions packages/backend/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PrismaClient, RepoIndexingJobType } from '@sourcebot/db';
import { createLogger, env, hasEntitlement, PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS } from '@sourcebot/shared';
import { createLogger, env, PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS } from '@sourcebot/shared';
import { hasEntitlement } from './entitlements.js';
import express, { Request, Response } from 'express';
import 'express-async-errors';
import * as http from "http";
Expand Down Expand Up @@ -100,7 +101,7 @@ export class Api {
}

private async triggerAccountPermissionSync(req: Request, res: Response) {
if (env.PERMISSION_SYNC_ENABLED !== 'true' || !hasEntitlement('permission-syncing')) {
if (env.PERMISSION_SYNC_ENABLED !== 'true' || !await hasEntitlement('permission-syncing')) {
res.status(403).json({ error: 'Permission syncing is not enabled.' });
return;
}
Expand Down
7 changes: 4 additions & 3 deletions packages/backend/src/ee/accountPermissionSyncer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as Sentry from "@sentry/node";
import { PrismaClient, AccountPermissionSyncJobStatus, Account, PermissionSyncSource} from "@sourcebot/db";
import { env, hasEntitlement, createLogger, loadConfig, PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS } from "@sourcebot/shared";
import { env, createLogger, loadConfig, PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS } from "@sourcebot/shared";
import { hasEntitlement } from "../entitlements.js";
import { ensureFreshAccountToken } from "./tokenRefresh.js";
import { Job, Queue, Worker } from "bullmq";
import { Redis } from "ioredis";
Expand Down Expand Up @@ -50,8 +51,8 @@ export class AccountPermissionSyncer {
this.worker.on('failed', this.onJobFailed.bind(this));
}

public startScheduler() {
if (!hasEntitlement('permission-syncing')) {
public async startScheduler() {
if (!await hasEntitlement('permission-syncing')) {
throw new Error('Permission syncing is not supported in current plan.');
}

Expand Down
7 changes: 4 additions & 3 deletions packages/backend/src/ee/repoPermissionSyncer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as Sentry from "@sentry/node";
import { PermissionSyncSource, PrismaClient, Repo, RepoPermissionSyncJobStatus } from "@sourcebot/db";
import { createLogger, PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "@sourcebot/shared";
import { env, hasEntitlement } from "@sourcebot/shared";
import { env } from "@sourcebot/shared";
import { hasEntitlement } from "../entitlements.js";
import { Job, Queue, Worker } from 'bullmq';
import { Redis } from 'ioredis';
import { createOctokitFromToken, getRepoCollaborators, GITHUB_CLOUD_HOSTNAME } from "../github.js";
Expand Down Expand Up @@ -44,8 +45,8 @@ export class RepoPermissionSyncer {
this.worker.on('failed', this.onJobFailed.bind(this));
}

public startScheduler() {
if (!hasEntitlement('permission-syncing')) {
public async startScheduler() {
if (!await hasEntitlement('permission-syncing')) {
throw new Error('Permission syncing is not supported in current plan.');
}

Expand Down
7 changes: 5 additions & 2 deletions packages/backend/src/ee/syncSearchContexts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ vi.mock('@sourcebot/shared', async (importOriginal) => {
error: vi.fn(),
debug: vi.fn(),
})),
hasEntitlement: vi.fn(() => true),
getPlan: vi.fn(() => 'enterprise'),
SOURCEBOT_SUPPORT_EMAIL: 'support@sourcebot.dev',
};
});

vi.mock('../entitlements.js', () => ({
hasEntitlement: vi.fn(() => Promise.resolve(true)),
getPlan: vi.fn(() => Promise.resolve('enterprise')),
}));

import { syncSearchContexts } from './syncSearchContexts.js';

// Helper to build a repo record with GitLab topics stored in metadata.
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/ee/syncSearchContexts.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import micromatch from "micromatch";
import { createLogger } from "@sourcebot/shared";
import { PrismaClient } from "@sourcebot/db";
import { getPlan, hasEntitlement, repoMetadataSchema, SOURCEBOT_SUPPORT_EMAIL } from "@sourcebot/shared";
import { repoMetadataSchema, SOURCEBOT_SUPPORT_EMAIL } from "@sourcebot/shared";
import { hasEntitlement } from "../entitlements.js";
import { SearchContext } from "@sourcebot/schemas/v3/index.type";

const logger = createLogger('sync-search-contexts');
Expand All @@ -15,10 +16,9 @@ interface SyncSearchContextsParams {
export const syncSearchContexts = async (params: SyncSearchContextsParams) => {
const { contexts, orgId, db } = params;

if (!hasEntitlement("search-contexts")) {
if (!await hasEntitlement("search-contexts")) {
if (contexts) {
const plan = getPlan();
logger.warn(`Skipping search context sync. Reason: "Search contexts are not supported in your current plan: ${plan}. If you have a valid enterprise license key, pass it via SOURCEBOT_EE_LICENSE_KEY. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}."`);
logger.warn(`Skipping search context sync. Reason: "Search contexts are not supported in your current plan. If you have a valid enterprise license key, pass it via SOURCEBOT_EE_LICENSE_KEY. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}."`);
}
return false;
}
Expand Down
23 changes: 23 additions & 0 deletions packages/backend/src/entitlements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {
Entitlement,
_hasEntitlement,
_getEntitlements,
} from "@sourcebot/shared";
import { prisma } from "./prisma.js";
import { SINGLE_TENANT_ORG_ID } from "./constants.js";

const getLicense = async () => {
return prisma.license.findUnique({
where: { orgId: SINGLE_TENANT_ORG_ID },
});
}

export const hasEntitlement = async (entitlement: Entitlement): Promise<boolean> => {
const license = await getLicense();
return _hasEntitlement(entitlement, license);
}

export const getEntitlements = async (): Promise<Entitlement[]> => {
const license = await getLicense();
return _getEntitlements(license);
}
5 changes: 3 additions & 2 deletions packages/backend/src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import * as Sentry from "@sentry/node";
import { getTokenFromConfig } from "@sourcebot/shared";
import { createLogger } from "@sourcebot/shared";
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type";
import { env, hasEntitlement } from "@sourcebot/shared";
import { env } from "@sourcebot/shared";
import { hasEntitlement } from "./entitlements.js";
import micromatch from "micromatch";
import pLimit from "p-limit";
import { processPromiseResults, throwIfAnyFailed } from "./connectionUtils.js";
Expand Down Expand Up @@ -124,7 +125,7 @@ const getOctokitWithGithubApp = async (
url: string | undefined,
context: string
): Promise<Octokit> => {
if (!hasEntitlement('github-app') || !GithubAppManager.getInstance().appsConfigured()) {
if (!await hasEntitlement('github-app') || !GithubAppManager.getInstance().appsConfigured()) {
return octokit;
}

Expand Down
Loading