Operate

Self-hosting

Pellucid runs on your laptop, on a VPS, or behind your own load balancer. Three components, two databases, one secret file.

The shipped stack is:

  • API — FastAPI + LangGraph + LiteLLM + spaCy. Python 3.12. Default port 8000.
  • Web — Next.js 15. Default port 3000.
  • Storage — SQLite by default (zero-config) or Postgres + pgvector for production. Optional Neo4j 5 for the ontology layer.

Local development

The fastest path. SQLite, no Neo4j, env-var BYOK.

bash
# Prereqs: Python 3.12+, Node 20+, pnpm 9+
git clone https://github.com/ashlr-ai/pellucid
cd pellucid

# JS deps
pnpm install

# Python deps + spaCy model
cd apps/api
uv sync
uv run python -m spacy download en_core_web_sm

# LLM key — see /docs/byok for all four providers
cp .env.example .env
# edit .env: set XAI_API_KEY (or another provider)

# Boot both servers from the repo root
cd ../..
pnpm dev:api   # http://localhost:8000
pnpm dev:web   # http://localhost:3000  (in a second terminal)

SQLite gives you a single-file database at apps/api/app.db. Delete the file to reset everything; commit migrations to Postgres when you outgrow it.

Postgres + pgvector

For production. The pgvector image bundles Postgres 16 with the vector extension already enabled, so you skip the CREATE EXTENSION permission dance. The compose file is in apps/api/.

apps/api
bash
docker compose -f docker-compose.postgres.yml up -d postgres

# Verify the container is healthy
docker compose -f docker-compose.postgres.yml ps

Point the API at it via env:

apps/api/.env
bash
PELLUCID_DB_URL=postgresql://pellucid:pellucid@localhost:5432/pellucid

Run the schema migrations:

apps/api
bash
uv run alembic upgrade head

# To create a new migration after a model change:
uv run alembic revision --autogenerate -m "describe the change"
SQLite vs Postgres. The SQLite path usescreate_all()on boot for a frictionless first run. Postgres is migration-only — Alembic owns the schema. Don’t mix the two on the same database.

Neo4j (optional)

The ontology layer is the only consumer of Neo4j. If you don’t plan to ingest approved-reference documents, skip it — every ontology endpoint responds without it (extraction runs in-memory and the results aren’t persisted).

apps/api
bash
docker compose -f docker-compose.neo4j.yml up -d

# Browser console: http://localhost:7474
# Bolt: bolt://localhost:7687
# Default credentials (DEV ONLY): neo4j / pellucid-dev

Configure the API to use it:

apps/api/.env
bash
NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=pellucid-dev

The compose file pre-installs APOC and gives Neo4j a 1 GB heap. Tune NEO4J_server_memory_heap_max__size upward in production — the default is fine for a dev box but tight for any real ontology.

Compose them all

Run the full local stack — Postgres, Neo4j — with one command. Either invoke both compose files or merge them into your own docker-compose.override.yml:

apps/api
bash
docker compose \
  -f docker-compose.postgres.yml \
  -f docker-compose.neo4j.yml \
  up -d

# Tail logs
docker compose -f docker-compose.postgres.yml -f docker-compose.neo4j.yml logs -f

The API and web app stay outside Compose for now — running them with pnpm dev gives faster reload than rebuilding containers on every change. For production, see the production deploy section below.

Environment variables

VariableDefaultNotes
PELLUCID_ENVdevelopmentSet to production for JSON-line logs.
PELLUCID_DB_URLSQLitePostgres URL when self-hosting.
PELLUCID_CORS_ORIGINShttp://localhost:3000Comma-separated. Add the prod web origin.
PELLUCID_LLM_MODELxai/grok-4Default model when no BYOK row exists.
XAI_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEYOne required. See BYOK.
NEO4J_URI / NEO4J_USER / NEO4J_PASSWORDOptional. Enables the ontology graph.

Production — API

The repo ships a multi-stage Dockerfile that bakes the spaCy model into the image, plus a railway.toml for Railway-native deploys. The same image runs anywhere that speaks OCI containers (Fly.io, Render, your own k8s cluster).

apps/api
bash
docker build -t pellucid/api:0.1.0 .

docker run --rm -p 8000:8000 \
  -e PELLUCID_ENV=production \
  -e PELLUCID_DB_URL=postgresql://... \
  -e PELLUCID_CORS_ORIGINS=https://pellucid.example.com \
  -e ANTHROPIC_API_KEY=sk-ant-... \
  -v /etc/pellucid:/secrets:ro \
  pellucid/api:0.1.0

Mount .pellucid_key from your secrets manager — the file holds the per-install Fernet key used to encrypt BYOK provider rows and webhook secrets at rest. Losing it breaks every encrypted row; leaking it lets an attacker decrypt them.

Production logging

PELLUCID_ENV=production switches the logger to one JSON object per line — friendly to Datadog, Loki, Cloud Logging, anything that ingests JSON. Pretty logs in dev; structured logs in prod, no flag choreography.

example production log line
text
{"ts":"2026-05-09T15:32:01.412Z","level":"INFO","logger":"uvicorn.error","msg":"Started server process [42]"}

Production — Web

The web app is a stock Next.js 15 build. Deploy to Vercel, Cloud Run, Fly, or self-host behind any reverse proxy.

apps/web
bash
pnpm build
pnpm start          # http://localhost:3000

# OR: containerize with whatever Next.js Dockerfile fits your platform

The web app expects the API origin in two env vars:

apps/web/.env.production
bash
# The API origin the browser will hit
NEXT_PUBLIC_API_URL=https://api.pellucid.example.com

# Used in metadata, OG/Twitter cards, sitemap
NEXT_PUBLIC_SITE_URL=https://pellucid.example.com

Production checklist

  1. Postgres, not SQLite. SQLite is safe for one user on one box; concurrent writes will block.
  2. Set PELLUCID_ENV=production so logs are JSON-line and your aggregator can parse them.
  3. Tighten CORS. Replace the dev wildcard with the exact web origin via PELLUCID_CORS_ORIGINS. Keep thechrome-extension://* regex if you use the browser extension (or pin to the published extension id).
  4. Mount .pellucid_key from your secrets manager. Back it up out-of-band. Rotation is manual today — re-saving every BYOK row + webhook re-encrypts under the new key.
  5. HTTPS terminates upstream. Run the API behind your reverse proxy of choice. Pellucid does not terminate TLS itself.
  6. Plan for retries. The webhook dispatcher is at-most- once and times out at five seconds — if the receiver is critical, mirror the payload into a queue receiver-side.
  7. Decide on auth. The Session 1 API runs unauthenticated. Mint API keys via /api/v1/integrations/api-keysnow so they’re ready for the auth middleware.

Sizing

For a single-tenant install with a handful of users:

  • API.1 vCPU, 1 GB RAM. spaCy and LangGraph fit comfortably; LLM calls are I/O-bound, not CPU-bound.
  • Web.0.5 vCPU, 512 MB RAM. Static-mostly with a handful of API proxy routes.
  • Postgres.1 vCPU, 1 GB RAM, 10 GB storage to start. Findings + debate transcripts dominate row count.
  • Neo4j.1 vCPU, 2 GB RAM. Skip entirely if you don’t use the ontology layer.

Scale agent throughput by raising the LLM provider’s rate limit. Pellucid runs four agents concurrently per analysis; if your provider caps at 60 RPM you’ll cap around 15 analyses per minute before rate-limit retries kick in.

Upgrading

  • Pull, install, migrate.
    bash
    git pull
    pnpm install
    cd apps/api && uv sync && uv run alembic upgrade head
  • Watch the changelog. Webhook payload schema bumps are breaking; schema_version reflects the contract.
  • Smoke-test the LLM key. Run a synthetic analyze right after a deploy — provider rate limits and key revocations are the most common silent failures.