Lucas BergerandClaude Sonnet 4.6 ee04aee4fb fix(20): CR-02 derive editorMember from live query data
Store only the member id in editorMemberId state rather than the full
AdminMember object. Derive editorMember from membersQuery.data after each
render, so query invalidations following a per-section save always give
MemberEditorSheet fresh server state — eliminating the stale-reference
silent-overwrite race.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:00:47 -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.

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 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 + 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 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%