- TypeScript 46.3%
- JavaScript 39.9%
- CSS 13.2%
- Dockerfile 0.5%
Moves both chat routes' Anthropic call/tool-loop and the composer's voice input onto packages shared with careboard/HomeManagement/ kittycharactersheet, so fixes (like the Android dictation-duplication bug) are maintained in one place instead of separately in each app. - server/index.js: both /api/chats/:id/messages and /api/shared/:id/messages replace their hand-rolled while(keepLooping) streaming/tool-loop with ClaudeClient.chat() from @brian/claude-chat-client, loaded via a lazy `await import(...)` (getClaudeClient()) since the package is ESM-only and this file is CommonJS - the same interop pattern already used here for mupdf. buildAttachmentBlock()/storedAttachmentBlock() are left untouched: they already produce the exact same content-block shapes the shared client's new image/pdf/text helpers do, so there's no benefit to replacing already-correct code. Required three additions to claude-chat-client to preserve exact behavior: onUsage (every round is separately billed, not just the final one), onToolUseBatch (one combined "fetching weather - searching web" status line instead of one per call, plus the exact assistant content array including any preamble text), and onToolResultBatch (for persisting the tool_result turn homeclaude's own richer message history needs). Also fixed the Node client's tool execution from parallel (Promise.all) to sequential, matching every real hand-rolled loop it replaces. - client/src/features/chat/components/Composer.tsx: useVoiceInput (hand-rolled, but had already independently converged on the same last-entry-only + fresh-instance-per-restart fix as the shared hook) replaced with @brian/claude-chat-ui's useSpeechToText. Deleted useVoiceInput.ts and the now-fully-dead speech-recognition.d.ts ambient types file. - Dockerfile, .forgejo/workflows/build-deploy.yml: both the client build stage (@brian/claude-chat-ui) and the backend stage (@brian/claude-chat-client) now depend on git.thenymans.com's npm registry rather than npmjs.com, threaded through as a BuildKit secret (docker buildx build, not plain docker build). Validated structurally against the real registry with dummy credentials (genuine 401s, not path/syntax errors). No existing test suite in this app to lean on, so this leaned harder on syntax checks, an isolated CJS/ESM interop test mirroring the exact dynamic-import pattern used, and manual code review preserving every original behavior than the careboard/HomeManagement migrations did. |
||
|---|---|---|
| .forgejo/workflows | ||
| client | ||
| server | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .npmrc | ||
| docker-compose.dev.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
Claude Family Chat
A self-hosted chat app that lets your family share access to Claude — each person gets their own private conversation history. Your API key stays on the server.
Quick Start
1. Get an API key
Sign up at console.anthropic.com and create an API key.
2. Edit docker-compose.yml
Open docker-compose.yml and fill in:
ANTHROPIC_API_KEY: "sk-ant-your-actual-key"
USERS: '{"alice":"password1","bob":"password2"}'
SESSION_SECRET: "any-long-random-string"
3. Run it
docker compose up -d
The app is now running at http://localhost:3000
On your home network, other devices can reach it at http://YOUR-LOCAL-IP:3000
(Find your IP with ipconfig on Windows or ifconfig / ip addr on Mac/Linux)
The Admin Page
Admins get a dedicated page at /admin (a ⚙ Admin link appears in the
sidebar). From there you can, without ever editing the deploy or restarting:
- Manage users — add a user directly (username + temporary password), or
generate a one-time invite link (
/join?token=…) the new person opens to set their own password. Delete users and grant/revoke admin. - View usage & cost — per-user token usage and per–group-chat usage (group chats are tracked separately), with period filters and cost estimates.
- Manage shared chats — create, edit (title / members / prompt), and delete.
- Edit configuration — the default system prompt, model, max-history, and the Anthropic & Brave API keys (masked, with reveal). Changes take effect immediately.
Everything here is stored in the SQLite database (/data/app.db).
Managing Users
Users and the admin flag are stored in a small SQLite database (/data/app.db),
with passwords hashed (scrypt) — they're never stored in plaintext. Day-to-day
user management happens on the Admin Page; the environment
variables below are only used to bootstrap an empty database on first run.
First run: set USERS and ADMIN_USER so the first accounts (including your
admin) exist. On the very first boot with an empty database, these are imported
automatically:
USERS: '{"mom":"secure-pass","dad":"another-pass","kid1":"their-pass"}'
ADMIN_USER: 'mom'
After the first boot the database is the source of truth — changes to
USERS / ADMIN_USER are ignored once any user exists. Manage users from the
Admin Page instead. (To re-seed from scratch, wipe the data volume with
docker compose down -v.)
Single sign-on (OIDC)
You can let people sign in through an OpenID Connect identity provider (Authentik, Authelia, Keycloak, Zitadel, Google, …) instead of — or in addition to — a username and password.
How it behaves:
- New people are provisioned automatically. The first time someone signs in
through your provider, an account is created for them (the username is derived
from their
preferred_username/email). On a brand-new install, the very first person to sign in this way becomes the admin. - Existing users can link their account. A password user opens the in-app Account panel (the person icon next to Sign out) and clicks Link single sign-on; from then on they can log in either way. Linking is the only way to attach SSO to an existing account — signing in with an unlinked provider identity always creates a separate account.
- OIDC-only accounts have no password until the user sets one in the Account panel. They can't unlink SSO until they do (it'd lock them out).
⚠️ Provisioning is open: anyone your identity provider authenticates gets an account here. Restrict who can authenticate (or which apps they can reach) at the provider, not in this app.
Setup: create an OIDC/OAuth2 confidential client in your provider with the redirect URI:
https://YOUR-DOMAIN/api/auth/oidc/callback
Then fill in the Single sign-on (OIDC) section on the Admin Page (or seed it
with the OIDC_* env vars on first boot). Once the issuer, client ID, and client
secret are all set, a Sign in with single sign-on button appears on the login
page. Requested scopes are openid email profile.
Environment Variables
Most settings now live in the database and are edited on the Admin Page.
The variables below seed the database on first boot only (when their value
isn't set in the DB yet); afterward the DB wins and the admin page is where you
change them. Only the infrastructure variables (SESSION_SECRET, DB_FILE,
PORT) are read from the environment on every boot.
| Variable | Default | Description |
|---|---|---|
ANTHROPIC_API_KEY |
(required first boot) | Your Anthropic API key. Seeds the DB, then editable on the Admin Page |
USERS |
{} |
JSON object of username→password pairs. First boot only — ignored once any user exists |
ADMIN_USER |
(none) | Username to mark as admin when seeding. First boot only |
BRAVE_API_KEY |
(none) | Brave Search API key (enables web search). Seeds the DB, then editable on the Admin Page |
CLAUDE_MODEL |
claude-sonnet-4-6 |
Default model. Seeds the DB, then editable on the Admin Page |
MAX_HISTORY |
100 |
Max messages kept per chat. Seeds the DB, then editable on the Admin Page |
SYSTEM_PROMPT |
(helpful-assistant prompt with tool guidance) | Default system prompt. Seeds the DB, then editable on the Admin Page |
OIDC_ISSUER |
(none) | OIDC provider issuer URL. Seeds the DB, then editable on the Admin Page |
OIDC_CLIENT_ID |
(none) | OIDC client ID. Seeds the DB, then editable on the Admin Page |
OIDC_CLIENT_SECRET |
(none) | OIDC client secret. Seeds the DB, then editable on the Admin Page |
OIDC_BUTTON_LABEL |
Single sign-on |
Label on the sign-on button. Seeds the DB, then editable on the Admin Page |
OIDC_BASE_URL |
(auto-detected) | Override for the base URL used to build the redirect URI. Seeds the DB, then editable on the Admin Page |
COMFYUI_URL |
(none) | ComfyUI HTTP API URL. When set, Claude gets a generate_image tool (the server must be able to reach this URL). Seeds the DB, then editable on the Admin Page |
COMFYUI_CHECKPOINT |
(none) | Checkpoint for the built-in default txt2img workflow. Seeds the DB, then editable on the Admin Page |
COMFYUI_STEPS |
25 |
Sampler steps for the built-in workflow. Seeds the DB, then editable on the Admin Page |
COMFYUI_CFG |
7 |
CFG scale for the built-in workflow. Seeds the DB, then editable on the Admin Page |
SESSION_SECRET |
change-me-in-production |
Secret for signing session cookies — change this! Read from env every boot |
DB_FILE |
/data/app.db |
Path to the SQLite database. Read from env every boot |
PORT |
3000 |
Port the server listens on. Read from env every boot |
Data & Privacy
- Conversation histories are saved in a Docker volume (
chat-data) — they persist across restarts - User accounts and runtime config (API keys, system prompt, model) live in a SQLite database (
/data/app.db) on the same volume; passwords are hashed (scrypt), never stored in plaintext - Each user's history is completely isolated from other users
- Your API key is only on the server; it's never sent to the browser (admins can reveal it on the Admin Page)
- To wipe all history and accounts:
docker compose down -v(deletes the volume)
Monitoring Costs
Keep an eye on your API usage at console.anthropic.com/usage. Set a spending limit under Settings → Limits to avoid surprises.
Updating
docker compose pull # if using a registry image
docker compose up -d --build
Running on a Raspberry Pi or Home Server
Works great on any always-on machine. Just open port 3000 in your firewall/router if you want access outside your home network (consider adding HTTPS via a reverse proxy like Caddy or nginx in that case).