Skip to main content

Local development

This page gets ContentsVision running on your machine. There are two processes (a FastAPI backend and a Next.js frontend) plus FFmpeg on your PATH.

Prerequisites

  • Python 3.11+
  • Node.js 20+
  • FFmpeg (brew install ffmpeg on macOS)
  • API keys: Anthropic / OpenRouter, Deepgram (transcription), and the AdjustSquare JWT secret for login
  • Optional: a PostHog project key to enable product analytics locally. Leave it unset and analytics stays off.

Quick start: ./start.sh

From the repo root, one command brings up both processes for development:

./start.sh

It is idempotent. On the first run it creates backend/venv, installs the backend and frontend dependencies, and copies backend/.env from backend/.env.example (it never overwrites an existing .env). Then it starts:

  • the backend on http://localhost:8001 (uvicorn --reload), and
  • the frontend on http://localhost:3001 (next dev).

Ctrl-C stops both cleanly. Useful flags:

  • --backend-only / --frontend-only run just one side.
  • --backend-port N / --frontend-port N use different ports (handy when the Docker local stack already holds 8001/3001). The frontend's /api proxy follows the chosen backend port automatically.
  • --skip-install skips the dependency checks for a faster restart.

After the first run, set the AI keys and ADJUSTSQUARE_JWT_SECRET in backend/.env (see the configuration reference); the script warns about any that are still empty. The sections below describe the same two processes by hand, which is exactly what the script automates.

Backend

cd backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

cp .env.example .env
# Edit .env: set the AI keys and ADJUSTSQUARE_JWT_SECRET (see the configuration reference)

uvicorn app.main:app --host 127.0.0.1 --port 8001 --reload

The backend now serves on http://localhost:8001, with interactive API docs at http://localhost:8001/docs. On first start it creates storage/inventory.db and runs the schema setup and backfills automatically.

Frontend

cd frontend
npm install
npm run dev

The dev server runs on http://localhost:3001 (the dev script is next dev -p 3001).

How the dev proxy works

The frontend calls everything under a relative /api. In development, next.config.js rewrites /api/* to the backend:

// frontend/next.config.js (rewrites fallback)
destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8001'}/api/:path*`

So with the backend on 8001 and no NEXT_PUBLIC_API_URL set, the proxy just works. The rewrite is a fallback, which lets the local SSE route handler (app/api/jobs/[id]/stream/route.ts) own the streaming endpoint while every other /api/* path falls through to the backend.

Signing in locally

There are two ways to sign in locally.

When APP_ENV=local and LOCAL_AUTH_USER_EMAIL are set in your .env, the backend skips JWT validation and authenticates as that user directly from the database.

# In backend/.env
APP_ENV=local
LOCAL_AUTH_USER_EMAIL=[email protected]

The user must already exist in the database. If you don't have any users yet, create one via the API or let the normal flow create it for you first, then update LOCAL_AUTH_USER_EMAIL to match their email.

Option 2: JWT token

If APP_ENV is not local, login goes through AdjustSquare SSO and requires a valid token signed with your ADJUSTSQUARE_JWT_SECRET. See the test helper make_test_token in backend/app/core/security.py for the exact token shape.

Running with Docker (optional)

The dev docker-compose.yml builds both services and publishes the backend on 8000 and the frontend on 3000. It passes NEXT_PUBLIC_API_URL=http://backend:8000 as a build arg so the frontend's /api proxy targets the backend container:

cp backend/.env.example .env # add your keys
docker compose up --build
Why a build arg, not just a runtime env var

The frontend builds with output: 'standalone', and Next.js freezes the next.config.js rewrite destination into the routes manifest at build time. A runtime-only NEXT_PUBLIC_API_URL is ignored by the proxy (it bakes the local-dev fallback and every /api/* call 500s with ECONNREFUSED). The value must be present during npm run build, so the frontend Dockerfile declares ARG NEXT_PUBLIC_API_URL and the compose files pass it under build.args.

If host ports 3000/8000 are already taken, use docker-compose.local.yml. It is the same stack on host ports 3001 (frontend) and 8001 (backend), and it loads the full .env:

docker compose -f docker-compose.local.yml up --build -d
# frontend → http://localhost:3001, backend → http://localhost:8001
docker compose -f docker-compose.local.yml logs -f # follow logs
docker compose -f docker-compose.local.yml down # stop and remove

This is closer to production but slower to iterate than the two-process setup above. For day-to-day development, prefer running uvicorn and next dev directly.

After every change: run the tests

This repo has a hard rule: run the full test suite after any change, and keep it green. From the repo root:

./run_tests.sh # everything
./run_tests.sh -k audio # filter by name
./run_tests.sh -x # stop at first failure

The suite is offline and safe to run anytime. See testing for the details and the rationale.

Common gotchas

  • ffmpeg not found: the video and audio pipelines shell out to FFmpeg. Install it and make sure it is on your PATH.
  • 401 on every request: your ADJUSTSQUARE_JWT_SECRET does not match the token you are sending. See troubleshooting.
  • Transcription does nothing: no DEEPGRAM_API_KEY set. The pipeline logs a warning and produces a visual-only inventory.
  • Port mismatch: if you run the backend on a port other than 8001, set NEXT_PUBLIC_API_URL so the proxy can find it.