A personal career, experience, and network manager.
  • TypeScript 96.4%
  • JavaScript 2.8%
  • Shell 0.4%
  • Dockerfile 0.2%
  • HTML 0.1%
Find a file
2026-06-30 14:00:11 +00:00
.forgejo/workflows test(e2e): cross-browser and mobile-viewport projects 2026-06-29 12:22:05 -05:00
.semgrep CI static analysis: typescript-eslint strict + Semgrep (#15) 2026-06-14 08:14:39 -05:00
apps Merge pull request 'fix(api): let native clients manage Personal Access Tokens (#386)' (#387) from 386-native-pat-management into main 2026-06-29 18:56:17 +00:00
docs fix(api): let native clients manage Personal Access Tokens (#386) 2026-06-29 13:41:28 -05:00
flatpak feat(client): flatpak manifest + desktop entry + icons (#188) 2026-06-21 12:41:48 -05:00
packages fix(client): clear native session on logout so it doesn't auto-sign-in 2026-06-29 13:23:04 -05:00
patches fix(android): bump foojay-resolver to 1.0.0 for Gradle 9 IBM_SEMERU removal (#278) 2026-06-24 08:33:26 -05:00
scripts chore(ci): teach package-age check to walk pnpm-lock.yaml (#213) 2026-06-23 07:49:58 -05:00
.dep-policy.json ci(security): flag <30d-old packages introduced by a PR (#136) 2026-06-19 12:25:14 -05:00
.dockerignore chore: restructure into pnpm workspaces — apps/api + placeholders (#181) 2026-06-20 22:16:03 -05:00
.gitignore test(e2e): shared session, db reset, and admin spec 2026-06-29 11:26:20 -05:00
.gitleaks.toml feat(auth): Personal Access Tokens — agent-runtime authentication (#49) 2026-06-19 11:57:37 -05:00
.tool-versions chore: restructure into pnpm workspaces — apps/api + placeholders (#181) 2026-06-20 22:16:03 -05:00
.trivyignore chore(security): ignore CVE-2026-12151 in Next.js bundled undici 2026-06-23 07:39:08 -05:00
CLAUDE.md docs: add Working discipline section to CLAUDE.md 2026-06-30 08:58:40 -05:00
cliff.toml ci(release): add tag-driven release pipeline (#16) 2026-06-16 21:04:39 -05:00
compose.postgres.yaml Containerize service + unauthenticated /api/health (#9) 2026-06-13 08:36:00 -05:00
compose.sqlite.yaml Containerize service + unauthenticated /api/health (#9) 2026-06-13 08:36:00 -05:00
CONTRIBUTING.md chore(client): scrub style={[...]} on DOM-leaf primitives + ESLint guardrail (#239) 2026-06-29 13:13:36 -05:00
cosign.pub build(release): add cosign public key 2026-06-17 09:13:11 -05:00
crypto.md Add at-rest encryption guidance doc 2026-06-12 16:51:07 -05:00
Dockerfile fix(docker): COPY patches/ into deps stage so pnpm install can apply patchedDependencies (#282) 2026-06-24 09:00:49 -05:00
forgejo-mcp.md added forgejo-mcp docuemntation 2026-06-12 15:37:30 -05:00
idea.md feat(api): link a Job/Contract's employer to a network Organization (#375) 2026-06-29 11:22:48 -05:00
lefthook.yml build: pin local + CI tool versions in .tool-versions (#157) 2026-06-19 20:07:22 -05:00
openapi.json fix(api): let native clients manage Personal Access Tokens (#386) 2026-06-29 13:41:28 -05:00
package.json fix(android): bump foojay-resolver to 1.0.0 for Gradle 9 IBM_SEMERU removal (#278) 2026-06-24 08:33:26 -05:00
pnpm-lock.yaml feat(api): LLM provider adapters — Anthropic + OpenAI-compatible 2026-06-28 19:18:03 -05:00
pnpm-workspace.yaml chore: restructure into pnpm workspaces — apps/api + placeholders (#181) 2026-06-20 22:16:03 -05:00
README.md docs: add the agent / MCP setup guide for self-hosters 2026-06-29 08:04:19 -05:00
renovate.json build: pin local + CI tool versions in .tool-versions (#157) 2026-06-19 20:07:22 -05:00

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 packageManager pin in package.json).
  • gitleaks — pre-commit secret scanner.
  • actionlint — pre-commit workflow linter for .forgejo/workflows/*.yml. Validates the schema and runs shellcheck against every run: 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 actionlint on macOS / Linuxbrew, or grab binaries from each project's releases page. Your version won't match .tool-versions exactly, 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/*.yml files — 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 varsBASE_REF, FORGEJO_API_URL, FORGEJO_TOKEN, the GITHUB_* set, PR_NUMBER, TEST_POSTGRES_URL, and Next's internal NEXT_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_URL default).
  • /app/data/storage/ — the blob store (CAROL_STORAGE_ROOT default).

Migrations apply on startup; first user to register becomes admin.

Postgres + OIDC + reverse proxy

APP_URL is mandatory for any deployment behind a reverse proxy. Carol builds the OAuth/OIDC redirect_uri from this value; without it the URL comes out as http://0.0.0.0:3000 and the IdP rejects sign-in. Match the host you've registered with each provider as the callback URL prefix. See docs/oidc-self-hoster-guide.md for 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.