- TypeScript 96.4%
- JavaScript 2.8%
- Shell 0.4%
- Dockerfile 0.2%
- HTML 0.1%
|
All checks were successful
Secrets / gitleaks (push) Successful in 37s
Reviewed-on: #394 |
||
|---|---|---|
| .forgejo/workflows | ||
| .semgrep | ||
| apps | ||
| docs | ||
| flatpak | ||
| packages | ||
| patches | ||
| scripts | ||
| .dep-policy.json | ||
| .dockerignore | ||
| .gitignore | ||
| .gitleaks.toml | ||
| .tool-versions | ||
| .trivyignore | ||
| CLAUDE.md | ||
| cliff.toml | ||
| compose.postgres.yaml | ||
| compose.sqlite.yaml | ||
| CONTRIBUTING.md | ||
| cosign.pub | ||
| crypto.md | ||
| Dockerfile | ||
| forgejo-mcp.md | ||
| idea.md | ||
| lefthook.yml | ||
| openapi.json | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| renovate.json | ||
Carol
A personal career, experience, and network manager. See idea.md for the
product spec and CLAUDE.md for conventions and stack guidance.
Contributors: see CONTRIBUTING.md for development
setup, local build recipes, and the workflow conventions in practice.
Requirements
The recommended path is mise: one tool to
install Node, pnpm, gitleaks, and actionlint at exactly the versions CI
runs (pinned in .tool-versions).
# install mise once (per the upstream guide); then in the repo:
mise install
That gets you:
- Node.js (currently 22.x LTS) and pnpm (matching the
packageManagerpin inpackage.json). - gitleaks — pre-commit secret scanner.
- actionlint — pre-commit
workflow linter for
.forgejo/workflows/*.yml. Validates the schema and runs shellcheck against everyrun:block.
Fallback if you'd rather install by hand:
- Node.js 20.9+ via your platform's package manager, then
corepack enable(ships with Node) to materialise the pinned pnpm. brew install gitleaks actionlinton macOS / Linuxbrew, or grab binaries from each project's releases page. Your version won't match.tool-versionsexactly, so CI may flag findings your local hook didn't (or vice versa).
Local development
pnpm install
pnpm install runs lefthook install, which wires up the git hooks in
lefthook.yml. After that, git commit automatically runs:
- gitleaks on every staged change — blocks the commit if a likely-secret pattern is found.
- actionlint on staged
.forgejo/workflows/*.ymlfiles — blocks the commit if a workflow schema, expression, or shell finding is detected. - commit-msg — rejects subjects that don't match Conventional Commits.
To skip a specific hook for one commit (prefer fixing the issue):
LEFTHOOK_EXCLUDE=gitleaks git commit ...
LEFTHOOK_EXCLUDE=actionlint git commit ...
LEFTHOOK_EXCLUDE=conventional git commit ...
pnpm -F @carol/api dev starts the Next.js dev server on
http://localhost:3000 against an in-repo SQLite file at
./data/carol.db. To override any of the variables in
Configuration for a local run, either export them in
your shell or put them in apps/api/.env.local:
# apps/api/.env.local (Next.js loads this automatically in dev)
DATABASE_URL=postgres://carol:carol@localhost:5432/carol
CAROL_STORAGE_ROOT=/tmp/carol-storage
Commands
Commands target the API workspace via pnpm -F @carol/api <script>. The
root package.json also exposes shortcuts that forward to the same
filtered invocation, so pnpm test from the repo root is equivalent to
pnpm -F @carol/api test.
| Command | What it does |
|---|---|
pnpm -F @carol/api dev |
Start the Next.js dev server on http://localhost:3000 |
pnpm -F @carol/api build |
Production build |
pnpm -F @carol/api start |
Run the production build |
pnpm -F @carol/api lint |
ESLint over the api workspace |
pnpm -F @carol/api typecheck |
TypeScript check (no emit) |
pnpm -F @carol/api test |
Vitest test run |
pnpm -F @carol/api format |
Prettier write |
pnpm -F @carol/api format:check |
Prettier check (no write) |
Configuration
Every self-hoster-facing environment variable Carol reads at runtime lives in the tables below. The container image ships sensible defaults for everything in Operations; everything else is opt-in.
CI-runner-only env vars —
BASE_REF,FORGEJO_API_URL,FORGEJO_TOKEN, theGITHUB_*set,PR_NUMBER,TEST_POSTGRES_URL, and Next's internalNEXT_RUNTIME— are not listed here. They live with the workflow or test that uses them.
Data
| Variable | Default | Notes |
|---|---|---|
DATABASE_URL |
sqlite:./data/carol.db |
SQLite path (prefix sqlite:) or a full postgres://… URL. Migrations apply on startup. SQLite is fine for single-user; switch to Postgres for multi-user or when you scale. |
CAROL_STORAGE_ROOT |
./data/storage |
Blob storage root. Profile pictures land at <root>/profile-pictures/<user_id>/avatar.webp. Must be writable by the service user. See ADR-0018. |
CAROL_ENCRYPTION_KEY |
(unset) | A base64- or hex-encoded 32-byte key used to encrypt each user's stored LLM provider API key at rest (AES-256-GCM). Required only to store per-user provider keys (Account → Carol's brain); the app boots fine without it and only the set-key path fails (with a clear 503) until it's set. Generate one with openssl rand -base64 32. Keep it secret and stable — rotating or losing it makes every stored provider key undecryptable. See ADR-0029 and the agent setup guide. |
Authentication
| Variable | Default | Notes |
|---|---|---|
APP_URL |
(request Origin) |
Canonical public URL of the instance (https://<host>, no trailing slash), used to build OAuth / OIDC redirect_uri. Required for any reverse-proxied deployment — without it Carol builds redirects from the container's bind address (http://0.0.0.0:3000) and IdPs reject the request. Applies to GitHub OAuth and every OIDC instance alike. |
REGISTRATION_POLICY |
open |
Who may register. open — anyone. invite — registration requires a one-time invite token an admin mints under Account → Invites (POST /api/auth/register rejects with invite_required / invite_invalid / invite_consumed / invite_expired otherwise). admin-approval — anyone may register but the account lands pending and can't sign in (pending_approval) until an admin approves it under Account → Pending approvals. Under every policy the first registration on a fresh instance is always allowed and becomes the admin. Unrecognised values fall back to admin-approval (fail closed). |
ACCESS_TOKEN_TTL_SECONDS |
900 (15 minutes) |
Lifetime of a bearer access token minted at POST /api/auth/token or rotated at POST /api/auth/refresh. Shorten for tighter revocation windows; native clients should refresh before this elapses. Bad values fall back to the default. See ADR-0027. |
REFRESH_TOKEN_TTL_SECONDS |
2592000 (30 days) |
Lifetime of a refresh token. After expiry the native client re-authenticates via POST /api/auth/token. Bad values fall back to the default. |
OIDC providers
OIDC providers are configured by env-var prefix: for each instance,
pick a <NAME> (matching [a-z0-9_]{1,32}) and declare a triplet plus
optional overrides. Multiple instances coexist. See
docs/oidc-self-hoster-guide.md for
the full walkthrough (Authentik, Keycloak, Zitadel, Google) and
ADR-0017 for the design.
| Variable | Required | Notes |
|---|---|---|
OIDC_<NAME>_ISSUER |
yes | OIDC issuer URL. Presence of this var triggers provider registration. |
OIDC_<NAME>_CLIENT_ID |
yes | Client id at the IdP. |
OIDC_<NAME>_CLIENT_SECRET |
yes | Client secret at the IdP. |
OIDC_<NAME>_LABEL |
no | Button label override (default: capitalised <NAME>). |
OIDC_<NAME>_AUTH_ENDPOINT |
no | Override discovery's authorize URL. |
OIDC_<NAME>_TOKEN_ENDPOINT |
no | Override discovery's token URL. |
OIDC_<NAME>_USERINFO_ENDPOINT |
no | Override discovery's userinfo URL. |
OIDC_<NAME>_JWKS_URI |
no | Override discovery's JWKS URI. |
OIDC_<NAME>_REQUIRE_EMAIL_VERIFIED |
no | true (default) requires the IdP to assert email_verified=true. Set false to opt out per-instance — only when the IdP can't be configured to assert it. |
Operations
| Variable | Default in image | Notes |
|---|---|---|
NODE_ENV |
production |
Standard Node value. Affects cookie secure flags and the React Query devtools (dev only). |
PORT |
3000 |
Listen port. |
HOSTNAME |
0.0.0.0 |
Listen address. |
NEXT_TELEMETRY_DISABLED |
1 |
Next.js sends no anonymous telemetry. Override to 0 only if you want to send it (unusual choice). |
SPA_BUNDLE_PATH |
(autodetected) | Override the path to the universal client's static bundle. By default Carol resolves /app/apps/client/dist (the location the release image ships it). Set this only if you stash the bundle elsewhere; pointing at a missing directory disables the SPA fallback and returns 404 for any non-/api/* path Next.js doesn't claim. |
Self-hosted deployment
The release image lives at forge.wynning.tech/james/carol:latest
(signed; verification command on each release page). Single container,
one volume for state.
podman run
podman volume create carol-data
podman run -d --name carol \
-p 3000:3000 \
-v carol-data:/app/data \
forge.wynning.tech/james/carol:latest
That's the whole deployment for a single-user SQLite instance. The volume holds:
/app/data/carol.db— the SQLite file (DATABASE_URLdefault)./app/data/storage/— the blob store (CAROL_STORAGE_ROOTdefault).
Migrations apply on startup; first user to register becomes admin.
Postgres + OIDC + reverse proxy
APP_URLis mandatory for any deployment behind a reverse proxy. Carol builds the OAuth/OIDCredirect_urifrom this value; without it the URL comes out ashttp://0.0.0.0:3000and the IdP rejects sign-in. Match the host you've registered with each provider as the callback URL prefix. Seedocs/oidc-self-hoster-guide.mdfor the full walkthrough.
podman run -d --name carol \
-p 3000:3000 \
-v carol-data:/app/data \
-e DATABASE_URL='postgres://carol:secret@postgres/carol' \
-e APP_URL='https://carol.example.com' \
-e OIDC_AUTHENTIK_ISSUER='https://auth.example.com/application/o/carol/' \
-e OIDC_AUTHENTIK_CLIENT_ID='carol' \
-e OIDC_AUTHENTIK_CLIENT_SECRET='…' \
forge.wynning.tech/james/carol:latest
The CAROL_STORAGE_ROOT default (./data/storage) still lands inside
the mounted volume; override only if you want the blobs elsewhere
(e.g. a separate NFS mount).
Compose
A minimal compose.yaml:
services:
carol:
image: forge.wynning.tech/james/carol:latest
ports:
- "3000:3000"
volumes:
- carol-data:/app/data
environment:
DATABASE_URL: postgres://carol:secret@postgres/carol
APP_URL: https://carol.example.com
postgres:
image: postgres:16
environment:
POSTGRES_USER: carol
POSTGRES_PASSWORD: secret
POSTGRES_DB: carol
volumes:
- carol-db:/var/lib/postgresql/data
volumes:
carol-data:
carol-db:
API
The running service publishes its OpenAPI 3.1 contract at
GET /api/openapi.json (public, no auth needed). A copy is committed
at openapi.json and regenerated from the route
handlers' zod schemas via pnpm -F @carol/api openapi:generate; CI
fails on drift between the committed copy and the live registry. Full
API conventions — status codes, error envelope, pagination, versioning —
live in docs/api-conventions.md.
Connect an external agent (MCP)
Carol exposes its agent tool surface over a streamable-HTTP MCP endpoint
at POST /api/mcp (JSON-RPC 2.0), so an external agent runtime (Claude
Code, opencode, a custom MCP client) can read and write your data. It is
authenticated with a Personal Access Token — create one in settings and
present it as a bearer token. With Claude Code:
claude mcp add --transport http carol https://carol.example.com/api/mcp \
--header "Authorization: Bearer carol_pat_…"
Tools are scoped to the token's user. Writes never apply directly: a
write tool returns a proposed change, and the agent applies it only after
you approve, via the commit_proposal tool (the confirmation step) — see
ADR-0029 and
ADR-0030. No new
configuration is required.
For step-by-step setup — enabling the built-in chat, connecting Claude Code / opencode / a generic MCP client, and reverse-proxy notes for the token stream — see the agent setup guide.
Layout
Carol is a pnpm workspace (ticket #181 / ADR-0027).
| Directory | Contents |
|---|---|
apps/api/ |
Next.js API service (@carol/api) — routes, lib, db, tests |
apps/client/ |
Expo universal client placeholder (@carol/client) — filled in by #183 |
packages/i18n/ |
Translation catalogs consumed by the universal client (@carol/i18n) |
packages/api-client/ |
Generated typed API client placeholder (@carol/api-client) — filled in by #182 |
scripts/ci/ |
Repo-level CI tooling (security summary, coverage, etc.) |
docs/ |
ADRs and operator deep-dive guides |
openapi.json |
Generated API contract (CI drift gate) |
Dockerfile |
Deployment unit — builds apps/api into a single image |
Project tracking
Work is tracked as issues on forge.wynning.tech/james/carol. Each PR
references the issue it implements.