Reviewed-on: #16
FamilySync
A self-hosted family organization hub for a two-person household. One color-coded calendar view across all family members' Fastmail calendars, plus shared collaborative lists (groceries, gift ideas) — delivered as a React PWA with no app store required.
What It Does
- Unified calendar — aggregates each member's Fastmail CalDAV calendars into a single color-coded view via tsdav + ical.js
- Shared lists — collaborative grocery and gift-idea lists with live sync via Server-Sent Events
- PWA — installable on iOS (Home Screen) and Android; push notifications via VAPID
- Single sign-on — all auth flows through your existing Authelia OIDC deployment
Prerequisites
- Node.js 22 LTS
- pnpm 11.5.1 (
corepack enable pnpm) - Docker + Docker Compose (for MariaDB, Redis, and production deployment)
Installation
git clone <repo-url> familysync
cd familysync
pnpm install
Copy the environment template and fill in values:
cp .env.example .env # then fill in values — see Environment Variables below
Required environment variables (set in .env or your Docker host):
| Variable | Description |
|---|---|
DB_PASSWORD |
MariaDB password for the familysync user |
DB_ROOT_PASSWORD |
MariaDB root password |
OIDC_ISSUER |
Authelia OIDC issuer URL |
OIDC_CLIENT_ID |
OIDC client ID (default: familysync) |
OIDC_CLIENT_SECRET |
OIDC client secret |
OIDC_REDIRECT_URI |
Callback URL registered in Authelia |
OIDC_AUTH_SECRET |
Random secret for session cookie signing |
APP_PASSWORD_ENCRYPTION_KEY |
Key used to encrypt stored Fastmail app passwords |
VAPID_PUBLIC_KEY |
VAPID public key (npx web-push generate-vapid-keys --json) |
VAPID_PRIVATE_KEY |
VAPID private key (never commit) |
VAPID_SUBJECT |
VAPID subject (mailto:you@example.com) |
Quick Start
Development (with hot-reload API and Vite HMR):
# Start backing services
docker compose -f docker-compose.yml -f docker-compose.dev.yml up mariadb redis
# Run migrations
pnpm --filter @familysync/api db:migrate
# Start API (in one terminal)
pnpm dev:api
# Start PWA (in another terminal)
pnpm dev:pwa
Production (Docker Compose):
docker compose up --build
The API listens on port 3000. The PWA build is served separately (Vite preview or a static host in front of the API container).
Monorepo Structure
apps/
api/ Hono backend — CalDAV sync, OIDC auth, lists API, push notifications
pwa/ React 19 PWA — calendar view, lists UI, service worker
docker-compose.yml Production services (API, MariaDB 11, Redis 7)
docker-compose.dev.yml Dev overrides (bind-mount src/, expose DB/Redis ports)
Commands
| Command | What it does |
|---|---|
pnpm dev:api |
Start API in watch mode (dist/ must be built first) |
pnpm dev:pwa |
Start Vite dev server with HMR |
pnpm build |
Build both api and pwa |
pnpm test |
Run API test suite (vitest + real MariaDB) |
pnpm test:e2e |
Run Playwright harness (iPhone + Pixel profiles) |
pnpm lint |
ESLint across all workspaces (flat config, TS-aware) |
pnpm typecheck |
Type-check all workspaces |
pnpm format |
Reformat all files with Prettier |
pnpm format:check |
Check formatting without writing (used in CI) |
pnpm --filter @familysync/api db:generate |
Generate Drizzle migration from schema changes |
pnpm --filter @familysync/api db:migrate |
Apply pending migrations to MariaDB |
Tech Stack
| Layer | Technology |
|---|---|
| Backend runtime | Node.js 22 + TypeScript, Hono 4.12.23 |
| Database ORM | Drizzle ORM 0.45.2 on MariaDB 11 (via mysql2) |
| Auth | @hono/oidc-auth 1.8.3 — authorization code + PKCE against Authelia |
| Calendar | tsdav 2.2.2 (CalDAV) + ical.js 2.2.1 against Fastmail |
| Push | web-push 3.6.7 (VAPID) |
| Live sync | Server-Sent Events + Redis 7 pub/sub |
| Frontend | React 19, Vite 8, vite-plugin-pwa 1.3, TanStack Query 5, Zustand 5 |
| Calendar UI | Schedule-X 4.6 |
Calendar Integration
FamilySync reads and writes calendars via CalDAV against Fastmail — not JMAP (not available for Fastmail calendars). Configure your Fastmail app password under the "Mail, Contacts & Calendars" scope. The principal URL follows the pattern:
https://caldav.fastmail.com/dav/principals/user/<your-fastmail-address>/
Store the app password in the database via the /me endpoint after first login.
Deployment
See docs/deployment.md for Unraid/Docker Compose deployment notes including the Pangolin/Newt tunnel configuration.
CI
Every PR to main must pass three required checks before it can merge:
| Job | What it runs |
|---|---|
CI / fast-checks |
pnpm lint, pnpm format:check, pnpm typecheck, PWA unit tests |
CI / api |
DB migrations + API test suite against a real MariaDB 11 service container |
CI / harness |
Playwright end-to-end harness (WebKit iPhone + Chromium Pixel) |
fast-checks and api/harness run in parallel. Defined in .gitea/workflows/ci.yml.
Publishing / Releases
Publishing happens automatically on every push to main — i.e. when a PR merges. The .gitea/workflows/publish.yml workflow runs and builds + pushes the API image to the Gitea container registry.
Image: git.bergerhouse.net/luckberg/familysync-api
Tags (two per release):
:latest— moving pointer for easy pulls:<MILESTONE>-<shortsha>— immutable, rollback-traceable (e.g.v1.1-98acff8)
Required secret: REGISTRY_PAT — a Gitea Actions secret holding a PAT with write:package scope. Named REGISTRY_PAT (not GITEA_*): Gitea reserves the GITEA_ prefix for secret names, so GITEA_-prefixed names cannot be created. GITEA_TOKEN / GITHUB_TOKEN cannot push packages.
Safety gate: Branch protection on main, not a needs: dependency in publish.yml. The PR test jobs (fast-checks, api, harness in ci.yml) run on pull_request — they never run in the same workflow invocation as publish.yml. Tests gate the PR; main is trusted to be green because direct push and force push are blocked and the three required checks (CI / fast-checks (pull_request), CI / api (pull_request), CI / harness (pull_request)) must pass before merge.
To bump the milestone tag at a milestone boundary: edit the MILESTONE env value at the top of .gitea/workflows/publish.yml.
License
Private — not open source.