Lucas BergerandClaude Opus 4.8 b6490feff4
CI / changes (pull_request) Successful in 9s
CI / api (pull_request) Successful in 3m2s
CI / fast-checks (pull_request) Successful in 4m20s
CI / security (pull_request) Successful in 1m14s
CI / harness (pull_request) Successful in 6m56s
CI / gate (pull_request) Successful in 2s
fix(19): satisfy CI fast-checks + secret scan
Lint (eslint --max-warnings 0):
- index.ts: disable no-unsafe-argument on the type-only Context mismatch when
  delegating to the OIDC handler inside the local-session skip wrapper
- localAuth.ts: handleLogout is sync (no await) — drop async (require-await)
- devBypass.ts: disable detect-possible-timing-attacks on the public well-known
  dev-placeholder string compare (not a secret comparison)
- remove dead code / unused bindings flagged by no-unused-vars: makeTestApp
  (localSession.test), makeUnauthContext + BrowserContext import (login.spec),
  unused memberId (admin.test), unused txSelectCount counter (me.test)
- localAuthMiddleware.test / me.test: fix unused + reflow-detached
  eslint-disable directives

Format: prettier --write across the 20 Phase-19 files that were never formatted.

Secret scan (gitleaks): allowlist two false positives — the synthetic >=32-char
TEST_SECRET in localSession.test.ts, and .planning/ design prose (a generic-api-key
regex hit on "credential atomically, 409-equivalent"). Neither is a real secret.

Verified locally: format:check, lint, typecheck, md:lint, gitleaks (no leaks),
PWA 266/266, API 452/452.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 23:05:15 -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 --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.

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