Repository: CapSoftware/Cap
Stars: 18182
CLAUDE.md
CLAUDE.md
This file provides comprehensive guidance to Claude Code when working with code in this repository.
Project Overview
Cap is the open source alternative to Loom. It's a Turborepo monorepo with a Tauri v2 desktop app (Rust + SolidStart) and a Next.js web app. The Next.js app at apps/web is the main web application for sharing and management; the desktop app at apps/desktop is the crossβplatform recorder/editor (macOS and Windows).
Product Context
- Core Purpose: Screen recording with instant sharing capabilities
- Target Users: Content creators, developers, product managers, support teams
- Key Features: Instant recording, studio mode, AI-generated captions, collaborative comments
- Business Model: Freemium SaaS with usage-based pricing
File Location Patterns & Key Directories
Core Applications
-
apps/web/ β Next.js web application (sharing, management, dashboard)-
apps/desktop/ β Tauri desktop app (recording, editing)-
apps/discord-bot/ β Discord integration bot-
apps/storybook/ β UI component documentationShared Packages
-
packages/database/ β Drizzle ORM, auth, email templates-
packages/ui/ β React components for web app-
packages/ui-solid/ β SolidJS components for desktop-
packages/utils/ β Shared utilities, types, constants-
packages/env/ β Environment variable validation-
packages/web-* β Effect-based web API layersRust Crates
-
crates/media*/ β Video/audio processing pipeline-
crates/recording/ β Core recording functionality-
crates/rendering/ β Video rendering and effects-
crates/camera*/ β Cross-platform camera handling-
crates/scap-*/ β Screen capture implementationsImportant File Patterns
-
/tauri.ts β Auto-generated IPC bindings (DO NOT EDIT)-
/queries.ts β Auto-generated query bindings (DO NOT EDIT)-
apps/web/actions//*.ts β Server Actions ("use server")-
packages/database/schema.ts β Database schema definitions-
.config. β Configuration files (Next.js, Tailwind, etc.)Key Commands
Development
pnpm dev:web # Start Next.js dev server (apps/web only)
pnpm run dev:desktop # Start Tauri desktop dev (apps/desktop)
pnpm build # Build all packages/apps via Turbo
pnpm lint # Lint with Biome across the repo
pnpm format # Format with Biome
pnpm typecheck # TypeScript project references buildDatabase Operations
pnpm db:generate # Generate Drizzle migrations
pnpm db:push # Push schema changes to MySQL
pnpm db:studio # Open Drizzle Studio
pnpm --dir packages/database db:check # Verify database schemaApp-Specific Commands
Web app (apps/web)
cd apps/web && pnpm dev # Start Next.js dev serverDesktop (apps/desktop)
cd apps/desktop && pnpm dev # Start SolidStart + Tauri dev
pnpm tauri:build # Build desktop app (release)Development Environment Guidelines
Server Management
- Do not start additional development servers or localhost services unless explicitly asked. Assume the developer already has the environment running and focus on code changes.
- Prefer
pnpm dev:web or pnpm run dev:desktop when you only need one app. Avoid starting multiple overlapping servers.- Avoid running Docker or external services yourself unless requested; root workflows handle them as needed.
- Database: MySQL via Docker Compose; schema managed through Drizzle migrations
- Storage: S3-compatible (AWS, Cloudflare R2, etc.) for video/audio files
Auto-generated Bindings (Desktop)
- NEVER EDIT:
tauri.ts, queries.ts (auto-generated on app load)- NEVER EDIT: Files under
apps/desktop/src-tauri/gen/- Icons: Auto-imported in desktop app; do not import manually
- Regeneration: These files update automatically when Rust types change
Common Development Pain Points
- Node Version: Must use Node 20 (specified in package.json engines)
- PNPM Version: Locked to 10.5.2 for consistency
- Turbo Cache: May need clearing if builds behave unexpectedly (
rm -rf .turbo)- Database Migrations: Always run
pnpm db:generate before pnpm db:push- Desktop Icons: Use
unplugin-icons auto-import instead of manual importsArchitecture Overview
Monorepo Structure
-
apps/web β Next.js 14 (App Router) web application-
apps/desktop β Tauri v2 desktop app with SolidStart (SolidJS)-
packages/database β Drizzle ORM (MySQL) + auth utilities-
packages/ui β React UI components for the web-
packages/ui-solid β SolidJS UI components for desktop-
packages/utils β Shared utilities and types-
packages/env β Zod-validated build/server env modules-
crates/* β Rust crates for media, rendering, recording, camera, etc.Technology Stack
- Package Manager: pnpm (
[email protected])- Build System: Turborepo
- Frontend (Web): React 19 + Next.js 14.2.x (App Router)
- Desktop: Tauri v2, Rust 2024, SolidStart
- Styling: Tailwind CSS (web consumes
@cap/ui/tailwind)- Server State: TanStack Query v5 on web;
@tanstack/solid-query on desktop- Database: MySQL (PlanetScale) with Drizzle ORM
- AI Integration: Groq preferred, OpenAI fallback; invoked in Next.js Server Actions
- Analytics: PostHog
- Payments: Stripe
Critical Architectural Decisions
1. AI on the Server: All Groq/OpenAI calls execute in Server Actions under
apps/web/actions. Never call AI from client components.2. Authentication: NextAuth with a custom Drizzle adapter. Session handling via NextAuth cookies; API keys are supported for certain endpoints.
3. API Surface: Prefer Server Actions. When routes are necessary, implement under
app/api/* (Hono-based utilities present), set proper CORS, and revalidate precisely.4. Desktop IPC: Use
tauri_specta for strongly typed commands/events; do not modify generated bindings.#### Desktop event pattern
Rust (emit):
use specta::Type;
use tauri_specta::Event;#[derive(Serialize, Type, tauri_specta::Event, Debug, Clone)]
pub struct UploadProgress {
progress: f64,
message: String,
}
UploadProgress { progress: 0.0, message: "Starting upload...".to_string() }
.emit(&app)
.ok();
Frontend (listen; generated bindings):
import { events } from "./tauri"; // auto-generated
await events.uploadProgress.listen((event) => {
// update UI with event.payload
});Development Workflow & Best Practices
Code Organization Principles
1. Follow Local Patterns: Study neighboring files and shared packages first
2. Database Changes: Always
pnpm db:generate β pnpm db:push β test3. Strict Typing: Use existing types; validate config via
@cap/env4. Component Consistency: Use
@cap/ui (React) or @cap/ui-solid (Solid)5. No Manual Edits: Never touch auto-generated bindings or schemas
Key Implementation Patterns
#### Server Actions (Web App)
"use server";import { db } from "@cap/database";
import { getCurrentUser } from "@cap/database/auth/session";
export async function updateVideo(data: FormData) {
const user = await getCurrentUser();
if (!user?.id) throw new Error("Unauthorized");
// Database operations with Drizzle
return await db().update(videos).set({ ... }).where(eq(videos.id, id));
}
#### Desktop IPC Commands
// Rust side - emit events
UploadProgress { progress: 0.5, message: "Uploading...".to_string() }
.emit(&app)
.ok();// Frontend side - listen to events (auto-generated)
import { events, commands } from "./tauri";// Call commands
await commands.startRecording({ ... });
// Listen to events
await events.uploadProgress.listen((event) => {
setProgress(event.payload.progress);
});
#### React Query Patterns
// Queries with Server Actions
const { data, isLoading } = useQuery({
queryKey: ["videos", userId],
queryFn: () => getUserVideos(),
staleTime: 5 60 1000,
});// Mutations with cache updates
const updateMutation = useMutation({
mutationFn: updateVideo,
onSuccess: (updated) => {
queryClient.setQueryData(["video", updated.id], updated);
},
});
Environment Variables
Build/Client (selected)
-
NEXT_PUBLIC_WEB_URL-
NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST-
NEXT_PUBLIC_DOCKER_BUILD (enables Next.js standalone output)Server (selected)
- Core:
DATABASE_URL, WEB_URL, NEXTAUTH_SECRET, NEXTAUTH_URL- S3:
CAP_AWS_BUCKET, CAP_AWS_REGION, CAP_AWS_ACCESS_KEY, CAP_AWS_SECRET_KEY, optional CAP_AWS_ENDPOINT, CAP_AWS_BUCKET_URL- AI:
GROQ_API_KEY, OPENAI_API_KEY- Email/Analytics:
RESEND_API_KEY, RESEND_FROM_DOMAIN, POSTHOG_PERSONAL_API_KEY, DUB_API_KEY, DEEPGRAM_API_KEY- OAuth:
GOOGLE_CLIENT_ID/SECRET, WORKOS_CLIENT_ID, WORKOS_API_KEY- Stripe:
STRIPE_SECRET_KEY_TEST, STRIPE_SECRET_KEY_LIVE, STRIPE_WEBHOOK_SECRET- CDN signing:
CLOUDFRONT_KEYPAIR_ID, CLOUDFRONT_KEYPAIR_PRIVATE_KEY- Optional S3 endpoints:
S3_PUBLIC_ENDPOINT, S3_INTERNAL_ENDPOINTTesting & Build Optimization
Testing Strategy
- Package-Specific: Check each
package.json for test commands- Web App: Uses Vitest for utilities, no comprehensive frontend tests yet
- Desktop: Vitest for SolidJS components in some packages
- Tasks Service: Jest for API endpoint testing
- Rust: Standard Cargo test framework for crates
Build Performance
- Turborepo Caching: Aggressive caching across all packages
- Cache Invalidation: Prefer targeted
--filter over global rebuilds- Docker Builds:
NEXT_PUBLIC_DOCKER_BUILD=true enables standalone output- Development: Incremental builds via TypeScript project references
Performance Monitoring
- Bundle Analysis: Check Next.js bundle size regularly
- Database Queries: Monitor with Drizzle Studio
- S3 Operations: Watch for excessive uploads/downloads
- Desktop Memory: Rust crates handle heavy media processing
Troubleshooting Common Issues
Build Failures
- "Cannot find module": Check workspace dependencies in package.json
- TypeScript errors: Run
pnpm typecheck to see project-wide issues- Turbo cache issues: Clear with
rm -rf .turbo- Node version mismatch: Ensure Node 20 is active
Database Issues
- Migration failures: Check
packages/database/migrations/meta/- Connection errors: Verify Docker containers are running
- Schema drift: Run
pnpm --dir packages/database db:checkDesktop App Issues
- IPC binding errors: Restart dev server to regenerate
tauri.ts- Rust compile errors: Check Cargo.toml dependencies
- Permission issues: macOS/Windows may require app permissions
- Recording failures: Verify screen capture permissions
Web App Issues
- Auth failures: Check NextAuth configuration and database
- S3 upload errors: Verify AWS credentials and bucket policies
- Server Action errors: Check network tab for detailed error messages
- Hot reload issues: Restart Next.js dev server
React/Next.js Coding Standards
Data Fetching & Server State
- Use TanStack Query v5 for all client-side server state and fetching.
- Use Server Components for initial data when possible; pass
initialData to client components and let React Query take over.- Mutations should call Server Actions directly and perform precise cache updates (
setQueryData/setQueriesData) rather than broad invalidations.Basic query pattern:
import { useQuery } from "@tanstack/react-query";function Example() {
const { data, isLoading, error } = useQuery({
queryKey: ["items"],
queryFn: fetchItems,
staleTime: 5 60 1000,
gcTime: 10 60 1000,
});
if (isLoading) return <Skeleton />;
if (error) return <ErrorState onRetry={() => { / refetch / }} />;
return <List items={data} />;
}
Server Action mutation with targeted cache updates:
"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateItem } from "@/actions/items"; // 'use server'function useUpdateItem() {
const qc = useQueryClient();
return useMutation({
mutationFn: updateItem,
onSuccess: (updated) => {
qc.setQueriesData({ queryKey: ["items"] }, (old: any[] | undefined) =>
old?.map((it) => (it.id === updated.id ? { ...it, ...updated } : it))
);
qc.setQueryData(["item", updated.id], updated);
},
});
}
Minimize useEffect usage: compute during render, handle logic in event handlers, and ensure cleanups for any subscriptions/timers.
Next.js App Router
- Prefer Server Components for SEO/initial rendering; hydrate interactivity in client components.
- Co-locate feature components, keep components focused, and use Suspense boundaries for long fetches.
UI/UX Guidelines
- Styling: Tailwind CSS only; stay consistent with spacing and tokens.
- Loading: Use static skeletons that mirror content; no bouncing animations.
- Performance: Memoize expensive work; code-split naturally; use Next/Image for remote assets.
Effect Patterns
Managed Runtimes
-
apps/web/lib/server.ts builds a ManagedRuntime from Layer.mergeAll so database, S3, policy, and tracing services are available to every request. Always run server-side effects through EffectRuntime.runPromise/runPromiseExit from this module so cookie-derived context and VideoPasswordAttachment are attached automatically.-
apps/web/lib/EffectRuntime.ts exposes a browser runtime that merges the RPC client and tracing layers. Client code should lean on useEffectQuery, useEffectMutation, and useRpcClient; never call ManagedRuntime.make yourself inside components.API Route Construction
- Next.js API folders under
apps/web/app/api/* wrap Effect handlers with @effect/platform's HttpApi/HttpApiBuilder. Follow the existing pattern: declare a contract class via HttpApi.make, configure groups/endpoints with Schema, and only export the handler returned by apiToHandler(ApiLive).- Inside
HttpApiBuilder.group blocks, acquire services (e.g., Videos, S3Buckets) with yield* inside Effect.gen. Provide layers using Layer.provide rather than manual provideService calls so dependencies stay declarative.- Map domain-level errors to transport errors with
HttpApiError.*. Keep error translation exhaustive (Effect.catchTags, Effect.tapErrorCause(Effect.logError)) to preserve observability.- Use
HttpAuthMiddleware for required auth and provideOptionalAuth when guests are allowed. The middleware/utility already hydrate CurrentUser, so avoid duplicating session lookups in route handlers.- Shared HTTP contracts that power the desktop app live in
packages/web-api-contract-effect; update them alongside route changes to keep schemas in sync.Server Components & Effects
- Server components that need Effect services should call
EffectRuntime.runPromise(effect.pipe(provideOptionalAuth)). This keeps request cookies, tracing spans, and optional auth consistent with the API layer.- Prefer lifting Drizzle queries or other async work into
Effect.gen blocks and reusing domain services (Videos, VideosPolicy, etc.) rather than writing ad-hoc logic.Client Integration
- React Query hooks should wrap Effect workflows with
useEffectQuery/useEffectMutation from apps/web/lib/EffectRuntime.ts; these helpers surface Fail/Die causes consistently and plug into tracing/span metadata.- When a mutation or query needs the RPC transport, resolve it through
useRpcClient() and invoke the strongly-typed procedures exposed by packages/web-domain instead of reaching into fetch directly.Desktop (Solid + Tauri) Patterns
- Data fetching:
@tanstack/solid-query for server state.- IPC: Call generated
commands and events from tauri_specta. Listen directly to generated events and prefer the typed interfaces.- Windowing/permissions are handled in Rust; keep UI logic in Solid and avoid mixing IPC with rendering logic.
Conventions
- CRITICAL: NO CODE COMMENTS: Never add any form of comments to code. This includes:
- Single-line comments:
// (JavaScript/TypeScript/Rust), # (Python/Shell)- Multi-line comments:
/ / (JavaScript/TypeScript), / / (Rust)- Documentation comments:
///, //! (Rust), / */ (JSDoc)- Any other comment syntax in any language
- Code must be self-explanatory through naming, types, and structure. Use docs/READMEs for explanations when necessary.
- Directory naming: lower-case-dashed
- Components: PascalCase; hooks: camelCase starting with
use- Strict TypeScript; avoid
any; leverage shared types- Use Biome for linting/formatting; match existing formatting
Rust Clippy Rules (Workspace Lints)
All Rust code must respect these workspace-level lints defined in
Cargo.toml. Violating any of these will fail CI:Rust compiler lints:
- unused_must_use = "deny" β Always handle Result/Option or types marked #[must_use]; never ignore them.
Clippy lints (all denied β code MUST NOT contain these patterns):
- dbg_macro β Never use dbg!() in code; use proper logging (tracing::debug!, etc.) instead.
- let_underscore_future β Never write let _ = async_fn() which silently drops futures; await or explicitly handle them.
- unchecked_time_subtraction β Use duration.saturating_sub(other) instead of duration - other to avoid panics on underflow.
- collapsible_if β Merge nested if statements: write if a && b { } instead of if a { if b { } }.
- clone_on_copy β Don't call .clone() on Copy types (integers, bools, etc.); just copy them directly.
- redundant_closure β Use function references directly: iter.map(foo) instead of iter.map(|x| foo(x)).
- ptr_arg β Accept &[T] or &str instead of &Vec<T> or &String in function parameters for flexibility.
- len_zero β Use .is_empty() instead of .len() == 0 or .len() > 0 / .len() != 0.
- let_unit_value β Don't assign () to a variable: write foo(); instead of let _ = foo(); or let x = foo(); when return is unit.
- unnecessary_lazy_evaluations β Use .unwrap_or(val) instead of .unwrap_or_else(|| val) when the default is a simple/cheap value.
- needless_range_loop β Use for item in &collection or for (i, item) in collection.iter().enumerate() instead of for i in 0..collection.len().
- manual_clamp β Use value.clamp(min, max) instead of manual if chains or .min(max).max(min) patterns.
Examples of violations to avoid:
dbg!(value);
let _ = some_async_function();
let duration = duration_a - duration_b;
if condition {
if other_condition {
do_something();
}
}
let x = 5.clone();
vec.iter().map(|x| process(x))
fn example(v: &Vec<i32>) { }
if vec.len() == 0 { }
let _ = returns_unit();
option.unwrap_or_else(|| 42)
for i in 0..vec.len() { println!("{}", vec[i]); }
value.min(max).max(min)Correct alternatives:
tracing::debug!(?value);
some_async_function().await;
let duration = duration_a.saturating_sub(duration_b);
if condition && other_condition {
do_something();
}
let x = 5;
vec.iter().map(process)
fn example(v: &[i32]) { }
if vec.is_empty() { }
returns_unit();
option.unwrap_or(42)
for item in &vec { println!("{}", item); }
value.clamp(min, max)Security & Privacy Considerations
Data Handling
- Video Storage: S3-compatible storage with signed URLs
- Database: MySQL with connection pooling via PlanetScale
- Authentication: NextAuth with custom Drizzle adapter
- API Security: CORS policies, rate limiting via Hono middleware
Privacy Controls
- Recording Permissions: Platform-specific (macOS Screen Recording, Windows)
- Data Retention: User-controlled deletion of recordings
- Sharing Controls: Password protection, expiry dates on shared links
- Analytics: PostHog with privacy-focused configuration
AI & Processing Pipeline
AI Integration Points
- Transcription: Deepgram API for captions generation
- Metadata Generation: Groq (primary) + OpenAI (fallback) for titles/descriptions
- Processing Location: All AI calls in Next.js Server Actions only
- Privacy: Transcripts stored in database, audio sent to external APIs
Media Processing Flow
Desktop Recording β Local Files β Upload to S3 β
Background Processing (tasks service) β
Transcription/AI Enhancement β Database StorageReferences & Documentation
Core Technologies
- TanStack Query: https://tanstack.com/query/latest
- React Patterns: https://react.dev/learn/you-might-not-need-an-effect
- Tauri v2: https://github.com/tauri-apps/tauri
- tauri_specta: https://github.com/oscartbeaumont/tauri-specta
- Drizzle ORM: https://orm.drizzle.team/
- SolidJS: https://solidjs.com/
Cap-Specific
- Self-hosting: https://cap.so/docs/self-hosting
- API Documentation: Generated from TypeScript contracts
- Architecture Decisions: See individual package READMEs
Development Resources
- Monorepo Guide: Turborepo documentation
- Effect System: Used in web-backend packages
- Media Processing: FFmpeg documentation for Rust bindings
Code Formatting
Always format code before completing work:
- TypeScript/JavaScript: Run pnpm format to format all code with Biome
- Rust: Run cargo fmt to format all Rust code with rustfmt
These commands should be run regularly during development and always at the end of a coding session to ensure consistent formatting across the codebase.
README.md
<p align="center">
<p align="center">
<img width="150" height="150" src="https://github.com/CapSoftware/Cap/blob/main/apps/desktop/src-tauri/icons/Square310x310Logo.png" alt="Logo">
</p>
<h1 align="center"><b>Cap</b></h1>
<p align="center">
The open source Loom alternative.
<br />
<a href="https://cap.so"><strong>Cap.so Β»</strong></a>
<br />
<br />
<b>Downloads for </b>
<a href="https://cap.so/download">macOS & Windows</a>
<br />
</p>
</p>
<br/>

Cap is the open source alternative to Loom. It's a video messaging tool that allows you to record, edit and share videos in seconds.
<img src="https://raw.githubusercontent.com/CapSoftware/Cap/refs/heads/main/apps/web/public/landing-cover.png"/>
Self Hosting
Quick Start (One Command)
git clone https://github.com/CapSoftware/Cap.git && cd Cap && docker compose up -dCap will be running at http://localhost:3000. That's it!
Note: Login links appear in the logs (docker compose logs cap-web) since email isn't configured by default.Other Deployment Options
| Method | Best For |
|--------|----------|
| Docker Compose | VPS, home servers, any Docker host |
| Railway | One-click managed hosting |
| Coolify | Self-hosted PaaS (use docker-compose.coolify.yml) |

Production Configuration
For production, create a .env file:
CAP_URL=https://cap.yourdomain.com
S3_PUBLIC_URL=https://s3.yourdomain.comSee our self-hosting docs for full configuration options including email setup, AI features, and SSL.
Cap Desktop can connect to your self-hosted instance via Settings β Cap Server URL.
Monorepo App Architecture
We use a combination of Rust, React (Next.js), TypeScript, Tauri, Drizzle (ORM), MySQL, TailwindCSS throughout this Turborepo powered monorepo.
A note about database: The codebase is currently designed to work with MySQL only. MariaDB or other compatible databases might partially work but are not officially supported.
Apps:
- desktop: A Tauri (Rust) app, using SolidStart on the frontend.
- web: A Next.js web app.
Packages:
- ui: A React Shared component library.
- utils: A React Shared utility library.
- tsconfig: Shared tsconfig configurations used throughout the monorepo.
- database: A React and Drizzle ORM Shared database library.
- config: eslint configurations (includes eslint-config-next, eslint-config-prettier other configs used throughout the monorepo).
License:
Portions of this software are licensed as follows:
- All code residing in the cap-camera and scap- families of crates is licensed under the MIT License (see licenses/LICENSE-MIT).
- All third party components are licensed under the original license provided by the owner of the applicable component
- All other content not mentioned above is available under the AGPLv3 license as defined in LICENSE
Contributing
See CONTRIBUTING.md for more information. This guide is a work in progress, and is updated regularly as the app matures.
Analytics (Tinybird)
Cap uses Tinybird to ingest viewer telemetry for dashboards. The Tinybird admin token (TINYBIRD_ADMIN_TOKEN or TINYBIRD_TOKEN) must be available in your environment. Once the token is present you can:
- Provision the required data sources and materialized views via pnpm analytics:setup. This command installs the Tinybird CLI (if needed), runs tb login when a .tinyb credential file is missing, copies that credential into scripts/analytics/tinybird, and finally executes tb deploy --allow-destructive-operations --wait from that directory. It synchronizes the Tinybird workspace to the resources defined in scripts/analytics/tinybird, removing any other datasources/pipes in that workspace.
- Validate that the schema and materialized views match what the app expects via pnpm analytics:check.
Both commands target the workspace pointed to by TINYBIRD_HOST (defaults to https://api.tinybird.co). Make sure you are comfortable with the destructive nature of the deploy step before running analytics:setup.