Self-Hosting
Self-Hosting FamilyHub
FamilyHub is fully open source. You can grab the code, run it on your own hardware, and keep full control of your data. It ships as plain OCI Docker images — build them once and run them anywhere Docker runs: a home server, a Raspberry Pi, a NAS, a mini-PC, a VPS, or a Kubernetes cluster. No runtime is special-cased.
Source code: gitlab.com/yvanpersonal/familyhub — MIT license.
Language
FamilyHub is multilingual. UI, AI assistant, and speech-to-text follow a per-family language preference — Dutch (NL) and English (EN) are supported today. See Multilingual support for details and how to add more languages.
Two ways to run FamilyHub
FamilyHub supports two deployment modes:
- Self-hosted (this guide) — You run it. No Stripe, no billing, no trial flows. All features unlocked. You supply your own AI / speech-to-text API keys if you want the AI assistant.
- Managed cloud — We run it at familyhub.business. You pay a monthly or yearly subscription, we handle hosting, AI credits, backups, and updates.
Both modes run the same codebase from the same repo. The payment module auto-disables when there's no Stripe secret key — nothing to configure, it just works. Your data schema is identical in both modes, so you can start self-hosted and migrate to managed later (or the reverse, by exporting your data).
The payment module (optional)
The Stripe-based payment module is disabled by default on self-hosted deployments. When disabled:
/api/payment/*endpoints aren't registered/api/store/trial/*trial signup endpoints aren't registered/api/webhooks/stripeisn't registered- No trial banner in the web UI, no subscription management page
- Families have unrestricted access to every feature — AI usage only gated by whether you configured an API key
You can confirm which mode you're running at startup — look for one of these lines in the logs:
FamilyHub payment module: ENABLED (SaaS mode)
FamilyHub payment module: DISABLED (self-hosted / OSS mode). All features unlocked, no billing UI, BYOK for AI/STT unless managed-AI env vars are set.
To force a specific mode regardless of Stripe config:
FAMILYHUB_PAYMENT_ENABLED=false # always disabled (OSS mode)
FAMILYHUB_PAYMENT_ENABLED=true # always enabled (requires Stripe config)
Without this variable, the runtime auto-detects from the presence of STRIPE_SECRET_KEY.
What you need
| Component | Version | Notes |
|---|---|---|
| JDK | Eclipse Temurin 24 | Kotlin 2.2 targets JDK 24 max |
| Docker | 24+ | For PostgreSQL + Radicale via Compose |
| Node.js | 20+ | For building and running the frontend |
| PostgreSQL | 18 | Included in Docker Compose |
| Radicale | Any recent version | Included in Docker Compose — for the shared calendar |
| Anthropic or OpenAI API key | — | For the AI assistant (optional, but it's the fun part) |
Quick start
Clone the repo, supply the two required secrets, and bring up the full stack with Docker Compose:
git clone https://gitlab.com/yvanpersonal/familyhub.git
cd familyhub
# Generate JWT_SECRET and ENCRYPTION_KEY — compose refuses to start without them
cp .env.example .env
printf 'JWT_SECRET=%s\nENCRYPTION_KEY=%s\n' \
"$(openssl rand -base64 32)" \
"$(openssl rand -base64 32)" > .env
# Build + start everything: Postgres, Radicale, API, web, Mailpit
docker compose up -d
Open http://localhost:5173 — you'll be prompted to create your Super Admin account.
The compose file maps:
| Service | Host port | Notes |
|---|---|---|
| Web UI | 5173 | nginx proxies /api and /ws to the api container |
| API | 8080 | direct access (health check, actuator) |
| Postgres | 5432 | persisted in the pgdata named volume |
| Radicale (CalDAV) | 5232 | persisted in the radicale-data named volume |
| Mailpit SMTP | 1025 | smtp://localhost:1025, no auth |
| Mailpit web UI | 8025 | trial signup emails in SaaS mode |
Running against the hosted-stack subset
If you'd rather do local development against ./mvnw spring-boot:run and npm run dev on the host — same compose file, just start the subset you need:
docker compose up -d postgres radicale mailpit
cd familyhub-api && ./mvnw spring-boot:run # terminal 1
cd familyhub-web && npm ci && npm run dev # terminal 2
Tip: Always use
./mvnw(the Maven wrapper), not a system-installedmvn. This ensures you're using the right version.
Load sample data
Want to see what it looks like with some data? Run the API with the local-dev profile instead of the compose-provided API:
docker compose up -d postgres radicale mailpit
cd familyhub-api && SPRING_PROFILES_ACTIVE=local-dev ./mvnw spring-boot:run
This creates sample family members, tasks, and shopping items on first startup.
Configuration
All config lives in familyhub-api/src/main/resources/application.yml. For local overrides (like API keys), create application-local-dev.yml in the same directory — it's gitignored so your keys stay safe.
Required environment variables
| Variable | Default | What it does |
|---|---|---|
JWT_SECRET | (required) | Secret for signing JWT tokens. Generate one with openssl rand -base64 32 |
ENCRYPTION_KEY | (required for services) | Encrypts API keys and service configs at rest. Generate with openssl rand -base64 32 |
DB_HOST | localhost | PostgreSQL host |
DB_PORT | 5432 | PostgreSQL port |
DB_NAME | familyhub | Database name |
DB_USER | familyhub | Database user |
DB_PASSWORD | familyhub | Database password |
Server & security
| Variable | Default | What it does |
|---|---|---|
SERVER_PORT | 8080 | API server port |
BASE_URL | http://localhost:8080 | Public URL of the API. Set this to your real domain in production |
CORS_ORIGINS | http://localhost:5173,http://localhost:3000 | Allowed frontend origins (comma-separated) |
COOKIE_SECURE | false | Set to true when running behind HTTPS |
CalDAV (shared calendar)
| Variable | Default | What it does |
|---|---|---|
CALDAV_URL | http://localhost:5232 | Radicale URL the backend uses (usually an internal/private address) |
CALDAV_USERNAME | local | Admin username used by the backend for MKCALENDAR/DELETE |
CALDAV_PASSWORD | local | Admin password |
CALDAV_HTPASSWD_PATH | (empty) | Path to the Radicale htpasswd file on shared storage. Required for per-family credentials — if unset, CalDAV falls back to shared admin auth |
AI assistant
You can set these globally via environment variables, or per-family in the web UI under Admin > Services. If you set ANTHROPIC_API_KEY globally, every family uses that key. If you don't, each family supplies their own via the admin UI (BYOK mode).
| Variable | Default | What it does |
|---|---|---|
ANTHROPIC_API_KEY | (optional) | Your Anthropic API key for Claude |
WHISPER_URL | https://api.openai.com | Whisper speech-to-text endpoint |
WHISPER_API_KEY | (optional) | API key for Whisper |
Payment module (only needed for SaaS mode)
Leave these unset for a normal self-hosted deployment. Only set them if you're running FamilyHub as a paid service and want the Stripe billing flows active.
| Variable | Default | What it does |
|---|---|---|
FAMILYHUB_PAYMENT_ENABLED | (auto) | true to force payments on, false to force off. Unset = auto-detect from STRIPE_SECRET_KEY |
STRIPE_SECRET_KEY | (empty) | Stripe API secret key. When set, payment module auto-enables |
STRIPE_WEBHOOK_SECRET | (empty) | Stripe webhook signing secret |
STRIPE_FULL_PRICE_ID | (empty) | Stripe Price ID for the monthly FULL plan |
STRIPE_FULL_PRICE_ID_ANNUAL | (empty) | Stripe Price ID for the annual FULL plan |
STRIPE_BYOK_PRICE_ID | (empty) | Stripe Price ID for the monthly BYOK plan |
STRIPE_BYOK_PRICE_ID_ANNUAL | (empty) | Stripe Price ID for the annual BYOK plan |
Generating secrets
# JWT secret
openssl rand -base64 32
# Encryption key (AES-256)
openssl rand -base64 32
Radicale (CalDAV)
Radicale is a lightweight CalDAV server that powers the shared family calendar. The included docker-compose.yml starts it automatically and creates the default calendar collection via an init container.
If you want to run it standalone — read the next section first: the default container image has non-obvious config requirements for Apple/iOS compatibility.
docker run -d \
-p 5232:5232 \
-v /data/radicale:/data \
-v $(pwd)/radicale/config:/config/config:ro \
-v $(pwd)/radicale/rights:/etc/radicale/rights:ro \
-v $(pwd)/radicale/users:/etc/radicale/users:rw \
tomsquest/docker-radicale
The calendar syncs with iOS (native), macOS Calendar (native), Android (via DAVx5), Proton Calendar (ICS feed), and Google Calendar (ICS feed).
Expose Radicale publicly so phones can reach it
Phones and macOS Calendar connect over the public internet, not your internal docker network. Point a public hostname (e.g. radicale.example.com) at your Radicale port through your reverse proxy, with a valid TLS certificate. iOS/macOS won't accept self-signed certs. Keep rate-limiting enabled at the proxy; don't add proxy-level basic auth — Radicale does its own per-family auth (see below).
Radicale must run its own auth — getting this wrong opens your calendar to the internet
The trap: the tomsquest/docker-radicale image reads its config from /config/config. If you mount your ConfigMap or config file anywhere else (a common mistake is /etc/radicale/config, since that's Radicale's documented default), Radicale silently falls back to upstream defaults — which are [auth] type=none. Anyone who knows a family UUID can read and write its calendar.
Verify by checking the pod/container logs at startup — you should see:
[INFO] Loaded config file '/config/config'
[INFO] auth type is 'radicale.auth.htpasswd'
[INFO] Read content of htpasswd file done ...
[INFO] rights type is 'radicale.rights.from_file'
If instead you see:
[WARNING] No user authentication is selected: '[auth] type=none' (INSECURE)
[INFO] rights type is 'radicale.rights.owner_only'
stop and fix the mount before exposing Radicale publicly.
Your /config/config should look like this:
[server]
hosts = 0.0.0.0:5232
[auth]
type = htpasswd
htpasswd_filename = /etc/radicale/users
htpasswd_encryption = bcrypt
[storage]
filesystem_folder = /data/collections
[rights]
type = from_file
file = /etc/radicale/rights
Rights rules for Apple CalDAV discovery
FamilyHub provisions each family at /family-<uuid>/events/, directly under that user's Radicale principal URL (/family-<uuid>/). This is required for Apple's RFC 6764 CalDAV discovery to find the calendar — iOS/macOS walk the principal tree, not arbitrary paths.
Your /etc/radicale/rights needs three rules:
# Backend admin — MKCALENDAR, DELETE, and cross-family admin operations.
# The username here must match CALDAV_USERNAME.
[admin]
user: familyhub
collection: .*
permissions: RrWw
# Apple/iOS CalDAV discovery PROPFINDs / for current-user-principal.
# Read-only on the empty path — no data is exposed, just the principal pointer.
[discovery-root]
user: family-(.+)
collection:
permissions: R
# Each family-<uuid> gets full access to its own principal tree only.
# Cross-family access (family-A trying /family-B/...) is denied.
[principal]
user: family-(.+)
collection: family-{0}(/.*)?
permissions: RrWw
The family-<uuid> htpasswd entries are written automatically by the FamilyHub backend when CALDAV_HTPASSWD_PATH is set to a writable file that Radicale also reads. The included Docker Compose bind-mounts that file between both containers.
Verifying the setup
After startup, test from outside the cluster/host:
# Anonymous must be 401, not 200.
curl -i -X PROPFIND https://radicale.example.com/ -H 'Depth: 0'
# Wrong password must be 401.
curl -i -X PROPFIND https://radicale.example.com/ -u 'family-<uuid>:wrong'
# Family creds must succeed (207) on their own path.
curl -i -X PROPFIND https://radicale.example.com/family-<uuid>/events/ \
-u 'family-<uuid>:<revealed-password>' -H 'Depth: 0'
# Family creds must 403 on another family's path.
curl -i -X PROPFIND https://radicale.example.com/family-<other-uuid>/ \
-u 'family-<uuid>:<revealed-password>'
All four must hold. If anonymous returns 200 or 207, Radicale isn't enforcing auth — go back to the "config at /config/config" step above.
Bring-your-own-key (BYOK) walkthrough
You don't need any env vars to run FamilyHub — the AI assistant, speech-to-text, CalDAV, and slideshow integrations can all be configured per-family from the admin UI. This is the normal path for small self-hosted setups where different families might use different providers.
- Log in as a Family Admin. Open Admin > Services.
- Pick the integration (AI / Speech-to-Text / CalDAV / Slideshow) and fill in the fields.
- Hit Test — FamilyHub calls the configured endpoint and reports the result.
- Save. Changes take effect on the next request — no restart needed.
Credentials are stored encrypted at rest (AES-256, via ENCRYPTION_KEY). Only the Family Admins of that family can see or edit them; other members just use the service.
When to prefer env vars: if you run a single family or want one set of credentials to cover every family on the deployment, the ANTHROPIC_API_KEY / WHISPER_API_KEY / CALDAV_URL env vars are a faster setup. The per-family UI always overrides the env-var defaults.
faster-whisper (voice input)
For fully self-hosted speech-to-text (no data leaves your network):
docker run -d \
-p 9000:9000 \
fedirz/faster-whisper-server:latest \
--model large-v3 \
--language nl
Add --gpus all if you have a GPU — makes transcription much faster. Without a GPU, consider using --model medium instead.
Then point FamilyHub to it:
- Via the UI: Admin > Services > Speech-to-Text → set URL to
http://your-server:9000 - Via env var:
WHISPER_URL=http://localhost:9000
Production deployment
For a production setup you'll want to:
- Run behind a reverse proxy with HTTPS (nginx, Traefik, Caddy, etc.)
- Set
COOKIE_SECURE=true - Set
CORS_ORIGINSandBASE_URLto your actual domain - Use real database credentials (not the defaults)
- Set up backups for PostgreSQL and Radicale data
- Make sure your reverse proxy forwards WebSocket connections at
/ws(HTTP/1.1 upgrade headers)
Health checks & monitoring
- Health check:
GET /api/health - Prometheus metrics:
GET /actuator/prometheus
Kubernetes (one of many options)
FamilyHub isn't tied to Kubernetes — the Docker Compose path above is a first-class way to run it — but it runs great on K8s too (including lightweight distributions like k3s, k0s, or MicroK8s). Bring your own manifests, Helm chart, Kustomize overlay, or GitOps flow (Flux, ArgoCD, Argo Rollouts — doesn't matter). Key things to know:
- All images run as non-root users
- PostgreSQL and Radicale need persistent volumes (any storage class)
- Ingress must forward WebSocket upgrades at
/ws(HTTP/1.1 upgrade support) - There are no custom operators, CRDs, or cluster-scoped resources required
Running tests
# Backend tests (needs Docker for Testcontainers)
cd familyhub-api
./mvnw clean verify
# Frontend tests
cd familyhub-web
npm test
# E2E tests (starts the full stack in Docker)
cd familyhub-e2e
npm test
Questions or issues?
Open an issue on GitLab or check the documentation.