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/stripe isn'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

ComponentVersionNotes
JDKEclipse Temurin 24Kotlin 2.2 targets JDK 24 max
Docker24+For PostgreSQL + Radicale via Compose
Node.js20+For building and running the frontend
PostgreSQL18Included in Docker Compose
RadicaleAny recent versionIncluded in Docker Compose — for the shared calendar
Anthropic or OpenAI API keyFor 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:

ServiceHost portNotes
Web UI5173nginx proxies /api and /ws to the api container
API8080direct access (health check, actuator)
Postgres5432persisted in the pgdata named volume
Radicale (CalDAV)5232persisted in the radicale-data named volume
Mailpit SMTP1025smtp://localhost:1025, no auth
Mailpit web UI8025trial 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-installed mvn. 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

VariableDefaultWhat 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_HOSTlocalhostPostgreSQL host
DB_PORT5432PostgreSQL port
DB_NAMEfamilyhubDatabase name
DB_USERfamilyhubDatabase user
DB_PASSWORDfamilyhubDatabase password

Server & security

VariableDefaultWhat it does
SERVER_PORT8080API server port
BASE_URLhttp://localhost:8080Public URL of the API. Set this to your real domain in production
CORS_ORIGINShttp://localhost:5173,http://localhost:3000Allowed frontend origins (comma-separated)
COOKIE_SECUREfalseSet to true when running behind HTTPS

CalDAV (shared calendar)

VariableDefaultWhat it does
CALDAV_URLhttp://localhost:5232Radicale URL the backend uses (usually an internal/private address)
CALDAV_USERNAMElocalAdmin username used by the backend for MKCALENDAR/DELETE
CALDAV_PASSWORDlocalAdmin 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).

VariableDefaultWhat it does
ANTHROPIC_API_KEY(optional)Your Anthropic API key for Claude
WHISPER_URLhttps://api.openai.comWhisper 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.

VariableDefaultWhat 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.

  1. Log in as a Family Admin. Open Admin > Services.
  2. Pick the integration (AI / Speech-to-Text / CalDAV / Slideshow) and fill in the fields.
  3. Hit Test — FamilyHub calls the configured endpoint and reports the result.
  4. 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:

  1. Run behind a reverse proxy with HTTPS (nginx, Traefik, Caddy, etc.)
  2. Set COOKIE_SECURE=true
  3. Set CORS_ORIGINS and BASE_URL to your actual domain
  4. Use real database credentials (not the defaults)
  5. Set up backups for PostgreSQL and Radicale data
  6. 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.