Lucas BergerandClaude Opus 4.8 23dc3cdd3f docs(readme): add app screenshots and Features section
Capture six PWA views (calendar, lists, list detail, login, setup wizard,
admin) into docs/screenshots/ and rebuild the README intro into a Features
section that expands the project description, pairing each capability with
its screenshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:24:13 -04:00

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

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

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

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

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

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

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

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

# 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)
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:

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 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
  • :<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, 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.

S
Description
No description provided
Readme
14 MiB
Languages
TypeScript 97.3%
JavaScript 1.6%
CSS 0.9%
Dockerfile 0.1%