# 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. ## Features ### One color-coded family calendar ![FamilySync month view showing each member's events in their own color alongside shared family events](docs/screenshots/calendar.png) Every member's personal Fastmail calendars and the shared family calendar are aggregated into a single month view. Each member gets a consistent color; the shared family calendar is always rose, so anyone can tell at a glance who has what on. Events read and write straight back to Fastmail over CalDAV (tsdav + ical.js), including recurring events, all-day events, and reminders — there is no second copy of your calendar to keep in sync. ### Shared collaborative lists ![Lists overview showing Groceries, Gift ideas, Costco run, and Hardware store with active and completed counts](docs/screenshots/lists.png) Groceries, gift ideas, weekend errands — create as many lists as the household needs, shared with everyone or kept personal. Each card shows live active/done counts so you know what's still outstanding before you leave the house. ![A grocery list detail view with checkable items, drag handles to reorder, and a completed section](docs/screenshots/list-detail.png) Inside a list, check items off, drag to reorder, and add new ones inline. Changes sync live to every other device over Server-Sent Events (with a polling fallback), so two people shopping together never duplicate or miss an item. ### Single sign-on for the whole household ![FamilySync sign-in screen with username and password fields plus a "Login with OIDC" option](docs/screenshots/login.png) Sign in with a local username and password, or through your existing Authelia OIDC deployment — no per-member calendar credential juggling. The PWA installs to the Home Screen on iOS and Android and supports push notifications (reminders and calendar-change alerts) via VAPID — no app store required. ### Guided, zero-setup onboarding ![First-run setup wizard with a four-step progress bar: Welcome, Instance, Calendar, Complete](docs/screenshots/setup.png) A first-run wizard walks the operator through configuring the instance — database, OIDC, VAPID keys, and the first Fastmail calendar connection — in about five minutes, so standing up a self-hosted instance does not require hand-editing config files. ### Household administration ![Admin settings showing the household members list with credential status and admin badges](docs/screenshots/admin.png) Admins manage members and their Fastmail credentials, assign each member a calendar color, choose which calendar is the shared family one, and set the household timezone — all from inside the app. ## Prerequisites - Node.js 22 LTS - pnpm 11.5.1 (`corepack enable pnpm`) - Docker + Docker Compose (for MariaDB and production deployment) ## Installation ```bash git clone familysync cd familysync pnpm install ``` Copy the environment template and fill in values: ```bash 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):** ```bash # Start backing services docker compose -f docker-compose.yml -f docker-compose.dev.yml up mariadb # 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):** ```bash 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 ```text 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) docker-compose.dev.yml Dev overrides (bind-mount src/, expose DB 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 md:lint` | Lint Markdown files with markdownlint-cli2 | | `pnpm generate-secrets` | Generate random secrets for `.env` setup | | `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 (in-process EventEmitter) | | 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: ```text https://caldav.fastmail.com/dav/principals/user// ``` Store the app password in the database via the `/me` endpoint after first login. ## Deployment See [`docs/deployment.md`](docs/deployment.md) for Unraid/Docker Compose deployment notes including the Pangolin/Newt tunnel configuration. ## CI Every PR to `main` must pass four jobs before it can merge: | Job | What it runs | | ------------------ | ---------------------------------------------------------------------------------- | | `CI / fast-checks` | `pnpm lint`, `pnpm format:check`, `pnpm md:lint`, `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) | | `CI / security` | Gitleaks secret scan (all PRs) + `pnpm audit` + outdated report (code PRs) | | `CI / gate` | Aggregate: asserts all jobs above passed or were legitimately skipped | `fast-checks` and `security` always run. `api` and `harness` are skipped for doc-only PRs (no changes outside `.gitea/`, `.planning/`, or `*.md`). The `gate` job is the single required check for merge. 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 - `:-` — 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`, `security`, `gate` 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 two required checks (`CI / fast-checks` and `CI / gate`) must pass before merge. `CI / api` and `CI / harness` are conditionally skipped on doc-only PRs and are gated via the always-running `CI / gate` aggregate. **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.