From 5d1e2e73760387e7c36594696dbb3eab0bf14fc7 Mon Sep 17 00:00:00 2001 From: Ed Spencer Date: Mon, 6 Jul 2026 23:46:40 -0400 Subject: [PATCH] docs: BYOK messaging across marketing site and technical docs Update the marketing site for the bring-your-own-key model: the how-it-works and get-started flows now cover both key paths (web Settings -> AI Provider for chat/document generation, and bragdoc llm set for CLI extraction), the FAQ gains a "Where do I put my API key?" entry, the CLI docs note that its key config only affects local extraction, and privacy pages/diagrams state that stored keys are AES-256-GCM encrypted at rest and only ever used server-side to call the provider the user chose. Stale copy implying BragDoc supplies the AI (privacy policy AI services list, "Our AI analyzes...", self-hosting env instructions) is corrected. Technical docs get a full BYOK architecture section in ai-integration.md (resolveModelForUser flow, NoLLMConfigError -> 409 no_llm_configured, the shared @bragdoc/ai package, verify-on-save, HKDF/AES-256-GCM crypto via BYOK_ENCRYPTION_KEY, and the embeddings-stay-on-platform-key v1 decision), and database.md documents the user_llm_config table and llm_provider enum. Remaining references to the removed model singletons (routerModel, documentWritingModel, getLLM) in api-conventions.md, FEATURES.md, and reports.md are updated. BYOK_ENCRYPTION_KEY is now listed alongside the other secrets in deployment.md, architecture.md, SELF_HOSTING.md, README.md, and CLAUDE.md, with OPENAI_API_KEY clarified as demo-mode/embeddings only. Co-Authored-By: Claude Fable 5 --- .claude/docs/tech/README.md | 6 +- .claude/docs/tech/ai-integration.md | 128 ++++++++++++++---- .claude/docs/tech/api-conventions.md | 2 +- .claude/docs/tech/architecture.md | 8 +- .claude/docs/tech/database.md | 53 ++++++++ .claude/docs/tech/deployment.md | 7 + CLAUDE.md | 3 +- README.md | 46 +++++-- apps/marketing/app/get-started/page.tsx | 8 +- .../components/cli-documentation.tsx | 11 ++ .../components/get-started/path-a-steps.tsx | 25 +++- .../how-it-works/privacy-architecture-v2.tsx | 2 +- .../how-it-works/privacy-architecture.tsx | 2 +- .../how-it-works/workflow-steps.tsx | 4 +- .../components/legal/privacy-policy.tsx | 16 +-- .../privacy/architecture-diagram.tsx | 1 + .../privacy/llm-provider-privacy.tsx | 22 ++- .../self-hosting/self-hosting-steps.tsx | 20 ++- .../why-it-matters/the-solution.tsx | 6 +- apps/marketing/lib/faq-data.ts | 5 + docs/FEATURES.md | 9 +- docs/SELF_HOSTING.md | 15 +- docs/reports.md | 2 +- 23 files changed, 314 insertions(+), 87 deletions(-) diff --git a/.claude/docs/tech/README.md b/.claude/docs/tech/README.md index 59f1ffde..463b02ac 100644 --- a/.claude/docs/tech/README.md +++ b/.claude/docs/tech/README.md @@ -151,14 +151,14 @@ export function ClientComponent({ data }: Props) { POSTGRES_URL=postgresql://... AUTH_SECRET= NEXTAUTH_URL=http://localhost:3000 -OPENAI_API_KEY=sk-... +BYOK_ENCRYPTION_KEY= # Encrypts per-user LLM API keys ``` ### Optional Variables ```env GOOGLE_CLIENT_ID=... # Google OAuth GITHUB_CLIENT_ID=... # GitHub OAuth -DEEPSEEK_API_KEY=... # Alternative LLM +OPENAI_API_KEY=sk-... # Platform key: demo mode + embeddings only DEMO_MODE_ENABLED=true # Demo features ``` @@ -188,7 +188,7 @@ pnpm lint # Lint all code ### File Locations - **Auth**: `apps/web/app/(auth)/auth.ts`, `apps/web/lib/getAuthUser.ts` - **Database**: `packages/database/src/schema.ts`, `packages/database/src/queries.ts` -- **AI**: `apps/web/lib/ai/extract-achievements.ts`, `apps/web/lib/ai/llm-router.ts` +- **AI**: `apps/web/lib/ai/extract-achievements.ts`, `apps/web/lib/ai/resolve-model.ts`, `packages/ai/` - **CLI**: `packages/cli/src/index.ts`, `packages/cli/src/commands/` - **API**: `apps/web/app/api/*/route.ts` diff --git a/.claude/docs/tech/ai-integration.md b/.claude/docs/tech/ai-integration.md index 8cbf12dd..43a7af5e 100644 --- a/.claude/docs/tech/ai-integration.md +++ b/.claude/docs/tech/ai-integration.md @@ -6,37 +6,100 @@ BragDoc uses the Vercel AI SDK v5 to integrate multiple LLM providers for achiev ## Supported Providers -### Web Application -- OpenAI (GPT-4, GPT-4o, GPT-3.5-turbo) +Both the web application and the CLI support the same six providers via the +shared `@bragdoc/ai` package: + +- OpenAI (GPT-4o, GPT-4.1, etc.) +- Anthropic Claude (Claude Sonnet, Opus) - Google Gemini (Gemini-1.5-pro, Gemini-2.0-flash) - DeepSeek (deepseek-chat) -- Anthropic Claude (planned) - -### CLI Tool -- OpenAI -- Google Gemini -- DeepSeek -- Anthropic Claude (Claude-3.5-Sonnet, Opus) -- Ollama (local models: llama3.2, qwen2.5-coder, etc.) +- Ollama (local models: llama3.2, qwen2.5-coder, etc. — no API key) - OpenAI-compatible APIs (LM Studio, LocalAI, etc.) -## LLM Router +## BYOK Architecture (Bring Your Own Key) -**File:** `apps/web/lib/ai/llm-router.ts` +BragDoc is free; all web AI features run on each user's own LLM API key. +There is no platform-key fallback for regular users — the old module-level +model singletons (`routerModel`, `documentWritingModel`, +`extractAchievementsModel`, `chatModel`) and the user-less `getLLM(taskType)` +helper were removed entirely. -Intelligently selects the appropriate LLM provider based on: -- Task type (extraction, generation, chat) -- Provider availability +### Shared Provider Package — `@bragdoc/ai` -```typescript -export async function getLLM( - user: User, - taskType: 'extraction' | 'generation' | 'chat' -): Promise { - // Selection logic based on user.level and taskType - // Returns configured model from @ai-sdk/* -} -``` +**Location:** `packages/ai/` + +Extracted from the CLI and now used by both the CLI and the web app: + +- `LLMProvider` union + per-provider config interfaces (`LLMConfig`) +- `DEFAULT_MODELS`, `PROVIDER_OPTIONS` (display names + API-key signup URLs) +- `createLLMFromConfig(config)` — provider factory (no env mutation; Google + uses `createGoogleGenerativeAI({ apiKey })`) +- `verifyLLMConfig(config)` — instantiates the model and runs a tiny + `generateText` probe (15s timeout); returns `{ ok } | { ok: false, error }`. + Never throws. Used by the settings "Verify & Save" flow. +- Zod-free public surface; built to `dist/` with declarations so the + published CLI can consume compiled output. + +### Model Resolution — `resolveModelForUser` + +**File:** `apps/web/lib/ai/resolve-model.ts` + +Every server-side AI call obtains its model through +`resolveModelForUser(user, task)` (task: `'chat' | 'extraction' | 'generation'`): + +1. **Demo users** (`user.isDemo === true`) → platform `OPENAI_API_KEY` with + the historical task→model mapping (extraction: gpt-4o-mini, generation: + gpt-4.1-mini, chat: gpt-4o). Demo keeps working with zero setup. +2. **Otherwise** → load the user's default `user_llm_config` row, decrypt + the stored API key, and build the model via `createLLMFromConfig`. The + user's single chosen model serves all tasks in v1. +3. **No config** → throw the typed `NoLLMConfigError`. + +Routes catch `NoLLMConfigError` and return HTTP 409 +`{ error: 'no_llm_configured' }` (via `noLLMConfigResponse()`). AI feature +surfaces (chat, document generation, standups, performance review, +workstreams) detect this code and show a "Connect your AI provider" CTA +linking to Settings. + +### Key Storage — `user_llm_config` Table + +One row per (user, provider); at most one `isDefault` row per user. Stores +`encryptedApiKey` + `iv` (base64, nullable for keyless providers like +Ollama), `keyHint` (last 4 chars for display), `model`, `baseURL`, and +`lastVerifiedAt`. See `database.md` for the full schema. + +### Encryption — `apps/web/lib/crypto/llm-keys.ts` + +- WebCrypto (`crypto.subtle`) AES-256-GCM with a random 12-byte IV per + encryption (Cloudflare Workers compatible). +- The AES key is derived via HKDF-SHA256 from the dedicated + `BYOK_ENCRYPTION_KEY` secret (deliberately NOT `BETTER_AUTH_SECRET`, so it + can rotate independently). +- Decryption happens strictly server-side inside route handlers / model + resolution. Plaintext keys never appear in API responses, logs, or client + components — `GET /api/user/llm-config` returns only masked metadata + (provider, model, keyHint, isDefault, lastVerifiedAt). + +### Verify-on-Save + +`PUT /api/user/llm-config` runs `verifyLLMConfig` with the raw key before +encrypting and storing it; verification failures are rejected with the +provider's error message. `lastVerifiedAt` records the last successful probe. + +### Embeddings Exception (v1 Decision) + +Workstreams embeddings stay on the platform `OPENAI_API_KEY`. Embeddings are +OpenAI-only (`text-embedding-3-small`, 1536-d pgvector column); Anthropic has +no embeddings API, and per-user embedding providers would mix vector +dimensions and invalidate stored vectors. Cost is negligible (~$0.02/1M +tokens), and Workstreams keeps working for Anthropic/Ollama users. Demo mode +is the only other platform-key consumer. + +### CLI Relationship + +The CLI keeps its own local BYOK config (`bragdoc llm set`, stored in +`~/.bragdoc/config.yml`) for local extraction — it does not use the key +stored in the web app. Both now share the `@bragdoc/ai` provider factory. ## Achievement Extraction @@ -46,10 +109,10 @@ Extracts achievements from Git commits using streaming: ```typescript import { streamObject } from 'ai'; -import { getLLM } from './llm-router'; +import { resolveModelForUser } from './resolve-model'; export async function* extractAchievements(commits: Commit[], user: User) { - const llm = await getLLM(user, 'extraction'); + const llm = await resolveModelForUser(user, 'extraction'); const { partialObjectStream } = streamObject({ model: llm, @@ -178,6 +241,11 @@ bragdoc llm set ollama llama3.2 # Set to local Ollama model ## Provider Configuration +In the web app, provider instantiation is handled by +`createLLMFromConfig` in `@bragdoc/ai` with the user's decrypted key — the +env-var examples below illustrate the underlying AI SDK calls (the CLI also +falls back to these env vars when no key is in its config). + ### OpenAI ```typescript import { createOpenAI } from '@ai-sdk/openai'; @@ -455,8 +523,9 @@ export const updatePerformanceReviewDocument = ({ user, dataStream }: UpdatePerf // 3. Stream updated content let draftContent = ''; + const model = await resolveModelForUser(user, 'generation'); const { fullStream } = streamText({ - model: documentWritingModel, + model, system: updateDocumentPrompt(document.content, 'text'), experimental_transform: smoothStream({ chunking: 'word' }), prompt: description, @@ -492,7 +561,8 @@ The tool is registered in the chat route using `createUIMessageStream`: const stream = createUIMessageStream({ execute: ({ writer: dataStream }) => { const result = streamText({ - model: routerModel, + model, // from resolveModelForUser(user, 'chat'); NoLLMConfigError → 409 + system: systemPrompt, messages: convertToModelMessages(uiMessages), tools: { @@ -552,5 +622,5 @@ const { messages, sendMessage, status } = useChat({ --- -**Last Updated:** 2026-01-13 +**Last Updated:** 2026-07-06 **Vercel AI SDK:** v5.0.0 diff --git a/.claude/docs/tech/api-conventions.md b/.claude/docs/tech/api-conventions.md index d48d87d8..0664df8d 100644 --- a/.claude/docs/tech/api-conventions.md +++ b/.claude/docs/tech/api-conventions.md @@ -785,7 +785,7 @@ The endpoint registers the `updatePerformanceReviewDocument` tool which enables const stream = createUIMessageStream({ execute: ({ writer: dataStream }) => { const result = streamText({ - model: routerModel, + model, // from resolveModelForUser(user, 'chat') system: enhancedSystemPrompt, messages: convertToModelMessages(uiMessages), tools: { diff --git a/.claude/docs/tech/architecture.md b/.claude/docs/tech/architecture.md index 85bf54fd..213b09ff 100644 --- a/.claude/docs/tech/architecture.md +++ b/.claude/docs/tech/architecture.md @@ -718,10 +718,10 @@ NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com MAILGUN_API_KEY=xxx MAILGUN_DOMAIN=mg.your-domain.com -# AI/LLM -OPENAI_API_KEY=sk-xxx -DEEPSEEK_API_KEY=xxx # Optional -GOOGLE_GENERATIVE_AI_API_KEY=xxx # Optional +# AI/LLM (BYOK) +BYOK_ENCRYPTION_KEY=xxx # Encrypts per-user LLM API keys at rest (openssl rand -base64 32) +OPENAI_API_KEY=sk-xxx # Platform key: demo mode + Workstreams embeddings only; + # all other AI features use each user's own key (Settings → AI Provider) ``` ### Optional Variables diff --git a/.claude/docs/tech/database.md b/.claude/docs/tech/database.md index 69b23e6b..4d194296 100644 --- a/.claude/docs/tech/database.md +++ b/.claude/docs/tech/database.md @@ -714,6 +714,59 @@ export const workstreamMetadata = pgTable('WorkstreamMetadata', { --- +### UserLLMConfig Table (BYOK) +**Purpose**: Per-user LLM provider configuration — Bring Your Own Key. Web AI features run on the key stored here (see `ai-integration.md` for the resolution flow). + +```typescript +export const llmProviderEnum = pgEnum('llm_provider', [ + 'openai', + 'anthropic', + 'google', + 'deepseek', + 'ollama', + 'openai_compatible', +]); + +export const userLLMConfig = pgTable( + 'user_llm_config', + { + id: uuid('id').primaryKey().notNull().defaultRandom(), + userId: uuid('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + provider: llmProviderEnum('provider').notNull(), + encryptedApiKey: text('encrypted_api_key'), // AES-256-GCM ciphertext, base64 (null for keyless providers) + iv: text('iv'), // 12-byte IV, base64 + keyHint: varchar('key_hint', { length: 8 }), // Last 4 chars for display; never return the key itself + model: varchar('model', { length: 256 }).notNull(), + baseURL: varchar('base_url', { length: 512 }), // Ollama / OpenAI-compatible endpoints + isDefault: boolean('is_default').notNull().default(false), + lastVerifiedAt: timestamp('last_verified_at'), // Last successful verify-on-save probe + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + userIdIdx: index('user_llm_config_user_id_idx').on(table.userId), + // One config per (user, provider) + userProviderUnique: uniqueIndex('user_llm_config_user_provider_unique').on( + table.userId, + table.provider, + ), + // At most one default config per user (partial unique index) + userDefaultUnique: uniqueIndex('user_llm_config_user_default_unique') + .on(table.userId) + .where(sql`${table.isDefault} = true`), + }), +); +``` + +**Key Points:** +- **Encryption**: `encryptedApiKey`/`iv` hold AES-256-GCM output from `apps/web/lib/crypto/llm-keys.ts` (key derived via HKDF-SHA256 from `BYOK_ENCRYPTION_KEY`). Both are nullable because keyless providers (Ollama) have no ciphertext. +- **Default row**: exactly one row per user may have `isDefault = true`, enforced by a partial unique index; `getDefaultLLMConfig(userId)` reads it during model resolution. +- **Account deletion**: `deleteAccountData()` hard-deletes rows explicitly — the user row is anonymized rather than deleted, so the FK cascade never fires on its own. + +--- + ### Session Table ```typescript export const session = pgTable('Session', { diff --git a/.claude/docs/tech/deployment.md b/.claude/docs/tech/deployment.md index 829090e6..a26e34fb 100644 --- a/.claude/docs/tech/deployment.md +++ b/.claude/docs/tech/deployment.md @@ -177,6 +177,7 @@ if (process.env.NODE_ENV === 'development') { AUTH_SECRET NEXTAUTH_URL OPENAI_API_KEY + BYOK_ENCRYPTION_KEY GOOGLE_CLIENT_ID GOOGLE_CLIENT_SECRET GITHUB_CLIENT_ID @@ -282,7 +283,13 @@ NEXT_PUBLIC_POSTHOG_KEY=phc_xxx # Same for both apps (cross-domain tracking) NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com # AI/LLM +# Platform key — only used for demo mode and Workstreams embeddings. +# Regular users' AI features run on their own keys (BYOK, added in Settings). OPENAI_API_KEY=sk-... + +# BYOK — encrypts user LLM API keys at rest (AES-256-GCM, key via HKDF-SHA256) +# Generate with: openssl rand -base64 32 +BYOK_ENCRYPTION_KEY=... ``` ### Optional diff --git a/CLAUDE.md b/CLAUDE.md index eababa6c..4e23a871 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -662,7 +662,8 @@ pnpm changeset version # Update versions - `POSTGRES_URL` - Database connection string - `BETTER_AUTH_SECRET` - Better Auth secret key (or `AUTH_SECRET` for backward compatibility) - `BETTER_AUTH_URL` - Application URL (or `NEXTAUTH_URL` for backward compatibility) -- `OPENAI_API_KEY` - OpenAI API key (for LLM) +- `BYOK_ENCRYPTION_KEY` - Encrypts per-user LLM API keys at rest (required for web AI features; generate with `openssl rand -base64 32`) +- `OPENAI_API_KEY` - Platform OpenAI key, used only for demo mode and Workstreams embeddings (all other AI features run on each user's own BYOK key from Settings → AI Provider) **Optional:** - `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` - Google OAuth diff --git a/README.md b/README.md index 6444809e..b1d3a223 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ BragDoc is a complete platform for managing your professional achievements: ### Key Features +- **🆓 Completely Free**: No tiers, no subscriptions — bring your own LLM API key +- **🔑 Bring Your Own Key**: AI features run on your own key (OpenAI, Anthropic, Google, DeepSeek, Ollama, or any OpenAI-compatible endpoint), added in Settings and encrypted at rest - **🎯 Achievement Tracking**: Log accomplishments individually or in batches - **🔗 Multi-Company Support**: Organize achievements as you change jobs - **📈 Impact Scoring**: AI-powered analysis ranks importance of your work @@ -80,7 +82,8 @@ The easiest way to get started: ```bash bragdoc login ``` -4. **Initialize your repository**: +4. **Add your AI API key**: In the web app, open **Settings → AI Provider** and add an API key for your chosen LLM provider (OpenAI, Anthropic, Google, DeepSeek, Ollama, or an OpenAI-compatible endpoint). This powers web AI features like chat and document generation — the key is verified on save and stored encrypted. The CLI configures its own key separately via `bragdoc llm set`. +5. **Initialize your repository**: ```bash cd /path/to/your/project bragdoc init @@ -124,9 +127,10 @@ Want to run your own instance? BragDoc is designed to be self-hosted. - Node.js 18+ - PostgreSQL database -- OpenAI API key (or other LLM provider) - Email service (Mailgun recommended) +No platform LLM key is required — your users bring their own (see below). An OpenAI key is only needed if you want demo mode and Workstreams embeddings. + ### Quick Setup 1. **Clone the repository**: @@ -158,7 +162,12 @@ Want to run your own instance? BragDoc is designed to be self-hosted. AUTH_SECRET="generate-with-openssl-rand-base64-32" NEXTAUTH_URL="http://localhost:3000" - # LLM Provider + # BYOK: encrypts the LLM API keys users add in Settings + # Generate with: openssl rand -base64 32 + BYOK_ENCRYPTION_KEY="..." + + # Platform LLM key (optional) - only used for demo mode and + # Workstreams embeddings; all other AI features use per-user keys OPENAI_API_KEY="sk-..." # Analytics (PostHog) @@ -280,14 +289,20 @@ Centralized data access with **Drizzle ORM**: ### AI/LLM Integration -**Vercel AI SDK** with intelligent routing: +**Vercel AI SDK** with per-user model resolution (BYOK): ```typescript -// Automatically selects optimal LLM based on task and user tier -const llm = await getLLM(user, 'extraction'); -const achievements = await extractAchievements(commits, llm); +// Resolves the user's own stored provider config (decrypted server-side). +// Demo users run on the platform key; users without a key get a 409 +// "no_llm_configured" and a "Connect your AI provider" CTA. +const llm = await resolveModelForUser(user, 'extraction'); ``` +Users add their LLM API key under **Settings → AI Provider**; keys are +verified on save and AES-256-GCM encrypted at rest (`BYOK_ENCRYPTION_KEY`). +The shared `@bragdoc/ai` package provides the provider factory for both the +web app and the CLI. + **Prompt Engineering**: - MDX-based prompts for maintainability @@ -296,9 +311,12 @@ const achievements = await extractAchievements(commits, llm); **Supported Providers**: -- OpenAI (GPT-4, GPT-3.5) -- DeepSeek +- OpenAI (GPT-4o, GPT-4.1) +- Anthropic (Claude) - Google (Gemini) +- DeepSeek +- Ollama (local, no API key) +- OpenAI-compatible endpoints (LM Studio, LocalAI, etc.) ### Data Flow @@ -306,11 +324,11 @@ const achievements = await extractAchievements(commits, llm); ``` 1. CLI reads Git commits locally -2. Sends batch to API: POST /api/cli/commits -3. API processes with LLM (extract meaningful achievements) -4. Saves to database with project/user association -5. Returns achievement IDs to CLI -6. CLI caches processed commit hashes +2. CLI extracts achievements locally using your configured LLM + (bragdoc llm set — key stays on your machine) +3. Sends finished achievements to the API +4. API saves them with project/user association +5. CLI caches processed commit hashes ``` #### Chat → Achievements diff --git a/apps/marketing/app/get-started/page.tsx b/apps/marketing/app/get-started/page.tsx index cd48fa0d..8f53f719 100644 --- a/apps/marketing/app/get-started/page.tsx +++ b/apps/marketing/app/get-started/page.tsx @@ -30,13 +30,17 @@ const getStartedSteps = [ name: 'Create Account', text: 'Set up your free account.', }, + { + name: 'Add Your AI API Key', + text: 'In the web app, open Settings → AI Provider and add your own API key (OpenAI, Anthropic, Google, DeepSeek, or a local Ollama endpoint). This powers web AI features like chat and document generation.', + }, { name: 'Login to BragDoc', text: 'Run bragdoc login to authenticate with your account.', }, { - name: 'Configure LLM', - text: 'Set up your preferred LLM provider (OpenAI, Anthropic, or local Ollama).', + name: 'Configure LLM for the CLI', + text: 'Run bragdoc llm set to configure the LLM provider the CLI uses for local extraction (OpenAI, Anthropic, or local Ollama).', }, { name: 'Initialize Repository', diff --git a/apps/marketing/components/cli-documentation.tsx b/apps/marketing/components/cli-documentation.tsx index a6b2dc31..68ed4ec2 100644 --- a/apps/marketing/components/cli-documentation.tsx +++ b/apps/marketing/components/cli-documentation.tsx @@ -547,6 +547,17 @@ bragdoc extract`} {/* LLM Configuration */}

LLM Configuration

+ +
+ + + The CLI's LLM configuration only affects local extraction + on your machine. Web app AI features (chat, document generation, + standups) use the API key you add in the web app under{' '} + Settings → AI Provider. + +
+
diff --git a/apps/marketing/components/get-started/path-a-steps.tsx b/apps/marketing/components/get-started/path-a-steps.tsx index 7efd3b6f..92f40312 100644 --- a/apps/marketing/components/get-started/path-a-steps.tsx +++ b/apps/marketing/components/get-started/path-a-steps.tsx @@ -40,6 +40,25 @@ export function PathASteps() { +
+ + + +
+
+ + - Your API key + Your API key, encrypted at rest
diff --git a/apps/marketing/components/how-it-works/privacy-architecture.tsx b/apps/marketing/components/how-it-works/privacy-architecture.tsx index 4b831d9d..3339a97b 100644 --- a/apps/marketing/components/how-it-works/privacy-architecture.tsx +++ b/apps/marketing/components/how-it-works/privacy-architecture.tsx @@ -106,7 +106,7 @@ export function PrivacyArchitecture() {
- Your API key + Your API key, encrypted at rest
diff --git a/apps/marketing/components/how-it-works/workflow-steps.tsx b/apps/marketing/components/how-it-works/workflow-steps.tsx index bd4ddc4c..8e838b39 100644 --- a/apps/marketing/components/how-it-works/workflow-steps.tsx +++ b/apps/marketing/components/how-it-works/workflow-steps.tsx @@ -49,7 +49,9 @@ const steps = [ icon: Settings, description: [ 'Choose your AI provider: OpenAI, Anthropic, Google, DeepSeek, or Ollama', - 'Ollama is completely free and runs locally', + 'For web features (chat, document generation): add your API key in Settings → AI Provider — it is verified on save and encrypted at rest', + 'For CLI extraction: run bragdoc llm set to configure a key locally', + 'Ollama is completely free and runs locally — no API key needed', 'Your API key, your costs, your control', ], codeBlocks: [{ code: 'bragdoc llm set', language: 'bash' }], diff --git a/apps/marketing/components/legal/privacy-policy.tsx b/apps/marketing/components/legal/privacy-policy.tsx index c0f8cf66..140aa0e3 100644 --- a/apps/marketing/components/legal/privacy-policy.tsx +++ b/apps/marketing/components/legal/privacy-policy.tsx @@ -215,16 +215,16 @@ export function PrivacyPolicyContent() {

AI Services

  • - OpenAI: AI-powered document generation and - achievement extraction + Your chosen LLM provider (OpenAI, Anthropic, + Google, DeepSeek, Ollama, or an OpenAI-compatible endpoint): + AI-powered document generation, chat, and achievement extraction, + using the API key you add in Settings. Your key is encrypted at + rest (AES-256-GCM) and only ever used server-side to call the + provider you selected.
  • - DeepSeek: Alternative AI provider for document - generation -
  • -
  • - Google Gemini: Alternative AI provider for - document generation + OpenAI: Embeddings for the Workstreams feature + and demo mode, using BragDoc's own API key
diff --git a/apps/marketing/components/privacy/architecture-diagram.tsx b/apps/marketing/components/privacy/architecture-diagram.tsx index 4d61ab92..762f481c 100644 --- a/apps/marketing/components/privacy/architecture-diagram.tsx +++ b/apps/marketing/components/privacy/architecture-diagram.tsx @@ -42,6 +42,7 @@ export function ArchitectureDiagram() {
  • • You choose: OpenAI, Anthropic, Google, Ollama
  • +
  • • Your API key, encrypted at rest (AES-256-GCM)
  • • Git metadata analyzed here
  • • Returns achievements only
diff --git a/apps/marketing/components/privacy/llm-provider-privacy.tsx b/apps/marketing/components/privacy/llm-provider-privacy.tsx index 912b6573..019e9a36 100644 --- a/apps/marketing/components/privacy/llm-provider-privacy.tsx +++ b/apps/marketing/components/privacy/llm-provider-privacy.tsx @@ -1,6 +1,6 @@ import { Card } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { ExternalLink, Sparkles, Server } from 'lucide-react'; +import { ExternalLink, Sparkles, Server, Lock } from 'lucide-react'; export function LlmProviderPrivacy() { const cloudProviders = [ @@ -84,6 +84,26 @@ export function LlmProviderPrivacy() {

+ + {/* API Key Security */} + +
+ +
+

+ Your API Key Is Encrypted +

+

+ API keys you add in Settings → AI Provider are encrypted at + rest with AES-256-GCM and only ever used server-side to call + the provider you chose. They are never returned to the + browser, logged, or shared. The CLI's key never leaves + your machine at all — it lives in{' '} + ~/.bragdoc. +

+
+
+
diff --git a/apps/marketing/components/self-hosting/self-hosting-steps.tsx b/apps/marketing/components/self-hosting/self-hosting-steps.tsx index ce383b4e..9369f98b 100644 --- a/apps/marketing/components/self-hosting/self-hosting-steps.tsx +++ b/apps/marketing/components/self-hosting/self-hosting-steps.tsx @@ -170,8 +170,9 @@ GITHUB_CLIENT_SECRET="your-github-client-secret" NEXT_PUBLIC_APP_URL="http://localhost:3000" NEXT_PUBLIC_MARKETING_URL="http://localhost:3101" -# AI Provider (at least one required for AI features) -OPENAI_API_KEY="sk-..."`} +# BYOK: encrypts the LLM API keys your users add in Settings +# Generate with: openssl rand -base64 32 +BYOK_ENCRYPTION_KEY="your-random-secret"`} /> @@ -183,7 +184,12 @@ OPENAI_API_KEY="sk-..."`} language="bash" code={`# Demo Mode (for testing) DEMO_MODE_ENABLED="false" -NEXT_PUBLIC_DEMO_MODE_ENABLED="false"`} +NEXT_PUBLIC_DEMO_MODE_ENABLED="false" + +# Platform OpenAI key - only used for demo mode and +# Workstreams embeddings. Regular AI features run on +# each user's own key, added in Settings -> AI Provider. +OPENAI_API_KEY="sk-..."`} /> @@ -191,9 +197,11 @@ NEXT_PUBLIC_DEMO_MODE_ENABLED="false"`}

Note: For a minimal self-hosted setup, you only need the database, authentication, one - OAuth provider, and one AI provider configured. All - other variables are optional and can be added as - needed. + OAuth provider, and the BYOK encryption key + configured. Users add their own LLM API keys via + Settings → AI Provider in the web app, exactly like on + bragdoc.ai. All other variables are optional and can + be added as needed.

diff --git a/apps/marketing/components/why-it-matters/the-solution.tsx b/apps/marketing/components/why-it-matters/the-solution.tsx index 19661dae..8c29051a 100644 --- a/apps/marketing/components/why-it-matters/the-solution.tsx +++ b/apps/marketing/components/why-it-matters/the-solution.tsx @@ -36,9 +36,9 @@ export function TheSolution() {

AI-Powered Insights

- Our AI analyzes your commits and generates clear, professional - descriptions of your achievements — ready to use in performance - reviews and manager reports. + Your own AI provider analyzes your commits and generates clear, + professional descriptions of your achievements — ready to use in + performance reviews and manager reports.

diff --git a/apps/marketing/lib/faq-data.ts b/apps/marketing/lib/faq-data.ts index 51ae5c84..4e559afe 100644 --- a/apps/marketing/lib/faq-data.ts +++ b/apps/marketing/lib/faq-data.ts @@ -27,6 +27,11 @@ export const faqData: FAQCategory[] = [ answer: "Ollama is free and runs locally on your machine. OpenAI and Anthropic are cloud-based options with excellent quality. It's your choice - all work great!", }, + { + question: 'Where do I put my API key?', + answer: + 'For web app AI features (chat, document generation, standups, performance reviews), add your key in Settings → AI Provider — it is verified when you save and stored encrypted at rest. The CLI configures its own key separately via `bragdoc llm set` for local extraction. Ollama needs no API key at all, just a base URL.', + }, { question: 'How long does setup take?', answer: diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 1064cb45..a3792245 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -201,11 +201,10 @@ Reports are stored in the `Document` table with the following key fields: #### Model Selection -Uses the `documentWritingModel` from the LLM router which selects the appropriate model based on: - -- Task type (document generation) -- Provider availability -- Cost optimization +Uses `resolveModelForUser(user, 'generation')` (BYOK): the model comes from +the user's own stored LLM provider config (Settings → AI Provider), decrypted +server-side. Demo users run on the platform OpenAI key; users without a +configured provider get a 409 `no_llm_configured` response. ### Security & Privacy diff --git a/docs/SELF_HOSTING.md b/docs/SELF_HOSTING.md index 12710729..e6cf0472 100644 --- a/docs/SELF_HOSTING.md +++ b/docs/SELF_HOSTING.md @@ -42,6 +42,7 @@ Your applications will be available at: - `DATABASE_URL`: PostgreSQL connection string - `NEXTAUTH_SECRET`: Random secret for session encryption (generate with `openssl rand -base64 32`) +- `BYOK_ENCRYPTION_KEY`: Random secret used to encrypt the LLM API keys your users add in Settings (AES-256-GCM at rest). Generate with `openssl rand -base64 32`. Use a different value from `NEXTAUTH_SECRET` so the two can rotate independently. ### Authentication Providers (Optional) @@ -53,9 +54,17 @@ Your applications will be available at: - `MAILGUN_API_KEY`: For sending emails - `MAILGUN_DOMAIN`: Your Mailgun domain -### AI Providers +### AI Providers (Bring Your Own Key) -- `OPENAI_API_KEY`: For OpenAI GPT models +BragDoc's AI features run on each user's own LLM API key. Users of your +self-hosted instance add a key under **Settings → AI Provider** in the web +app (OpenAI, Anthropic, Google, DeepSeek, Ollama, or any OpenAI-compatible +endpoint) — exactly like on bragdoc.ai. Keys are verified on save and stored +encrypted with `BYOK_ENCRYPTION_KEY`. Ollama needs no key, just a base URL. + +- `OPENAI_API_KEY` (optional): A platform-level OpenAI key that is only used + for demo mode and Workstreams embeddings. Everything else uses per-user + keys, so you can skip this if you don't need those two features. ## Deployment Options @@ -123,7 +132,7 @@ BragDoc is completely free — there is no payment integration or feature gating - ✅ Unlimited achievement tracking - ✅ Document generation -- ✅ AI assistance (if API keys provided) +- ✅ AI assistance (each user adds their own LLM API key in Settings) - ✅ Email integration - ✅ GitHub integration - ✅ All analytics and exports diff --git a/docs/reports.md b/docs/reports.md index 3c166fa9..96fc7cfa 100644 --- a/docs/reports.md +++ b/docs/reports.md @@ -590,7 +590,7 @@ interface GenerateDocumentFetcherProps { - Returns rendered prompt string 3. **Execute** (`execute()` function) - - Calls `streamText()` with `documentWritingModel` + - Calls `streamText()` with the model from `resolveModelForUser(user, 'generation')` - Streams response from LLM - Returns async iterable