Files
familysync/.planning/research/STACK.md
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

38 KiB

Stack Research

Domain: Self-hosted family calendar + shared-lists PWA on Fastmail Researched: 2026-06-03 (v1.0) / 2026-06-10 (v1.1 additions) Confidence: MEDIUM-HIGH (calendar sharing cross-account caveat: LOW; rest HIGH)


v1.1 Stack Additions — Operability & Polish

This section covers ONLY what is new for v1.1. The rest of the file (below) documents the v1.0 stack, which is unchanged.

What needs NO new dependency

Feature Existing tool that covers it Why no addition needed
Per-event reminders (VALARM) ical.js + tsdav + existing write path VALARM is a VCALENDAR component; ical.js parses/emits it; tsdav handles the PUT. No new library.
Outbox drain event-driven wake ioredis pub/sub (already in stack) Publish a caldav:drain event on Redis after a write; outbox worker subscribes and drains immediately. Zero new deps.
Admin Settings UI (app passwords + shared calendar) Existing Drizzle schema + AES-256-GCM crypto (already in apps/api) Role-gated Hono routes + React form. Schema already has the tables.
Setup wizard — DB connectivity probe mysql2 (already in stack) Attempt a mysql2 connect with the env-supplied credentials; resolve/reject gives pass/fail.
Setup wizard — VAPID key validation web-push + Node.js built-in crypto (already in stack) Buffer.from(key, 'base64url').length === 32 for the private key; web-push.generateVAPIDKeys() for a fresh keypair; no extra library.
Setup wizard — OIDC discovery probe Node.js 22 built-in fetch fetch(issuer + '/.well-known/openid-configuration') and check for 200 + authorization_endpoint field. Native fetch in Node 22; zero extra library.
Setup wizard — env-var presence checks zod (already in stack) A z.object({...}).safeParse(process.env) at startup is the entire validation. Already used for request body validation.

What IS new for v1.1

Two additions only: @playwright/test for the mobile test harness, and the Gitea Actions workflow files (YAML only — no new runtime dep).


New: @playwright/test (dev dependency, apps/pwa)

Purpose: Mobile-viewport + device-emulation + authenticated test harness. The existing playwright-cli global binary is an interactive/agentic tool not designed for CI spec files — it does not expose storageState save/restore, device emulation presets (devices['iPhone 15 Pro']), or a programmatic config (playwright.config.ts) needed to run mobile tests on a self-hosted runner.

Package: @playwright/test Current version: 1.60.0 (verified npm, June 2026) Install scope: devDependencies in apps/pwa only (not the monorepo root; only the PWA workspace needs browser tests).

Why this and not playwright-cli alone:

  • playwright-cli (the global binary) does not support storageState file save/restore — the mechanism required to inject an Authelia session into a test context without re-running the full OIDC redirect flow on every test run.
  • @playwright/test provides devices registry (iPhone 15 Pro, Pixel 5, etc.) which sets viewport, userAgent, isMobile, hasTouch together as a named preset.
  • @playwright/test is the only path to a playwright.config.ts that defines a setup project (do login once, write storageState to .auth/user.json) and a mobile project that consumes it — the pattern needed for an authenticated, mobile-emulated CI run against the DEV_AUTH_BYPASS entry point.
  • playwright-cli and @playwright/test coexist: playwright-cli continues to be the interactive verification tool during development; @playwright/test is the CI spec runner.

Authentication strategy for OIDC-gated PWA:

Authelia cannot be bypassed in a normal CI environment. The approach is to use the existing DEV_AUTH_BYPASS=true env flag (already implemented in apps/api) which injects user 1's session without an OIDC redirect. The setup project navigates to the app with DEV_AUTH_BYPASS active, waits for the authenticated state, then calls context.storageState({ path: '.auth/user.json' }). All subsequent test projects set storageState: '.auth/user.json' in their use config. This avoids any need to mock Authelia or run a real OIDC provider in CI.

Device presets to use:

// playwright.config.ts (apps/pwa)
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'mobile-safari',
      use: { ...devices['iPhone 15 Pro'], storageState: '.auth/user.json' },
      dependencies: ['setup'],
    },
    {
      name: 'mobile-chrome',
      use: { ...devices['Pixel 5'], storageState: '.auth/user.json' },
      dependencies: ['setup'],
    },
  ],
});

Version compatibility: @playwright/test@1.60.0 — installs its own browser binaries. In CI (Gitea Actions), use npx playwright install --with-deps chromium in the workflow to install only Chromium (smallest footprint). The catthehacker/ubuntu:act-latest job container includes Node 20+ and system deps needed by Playwright.

Do NOT install: playwright (the library package) separately — @playwright/test bundles it. Do not install @playwright/test at the monorepo root; it belongs only in apps/pwa.


New: Gitea Actions workflow files (.gitea/workflows/)

No new runtime npm packages. Workflow files are YAML only.

Syntax compatibility: Gitea Actions uses the same YAML syntax as GitHub Actions (on:, jobs:, steps:, services:, uses:). Workflow files live in .gitea/workflows/ (not .github/workflows/). GitHub Actions actions (actions/checkout@v4, docker/login-action@v3, docker/build-push-action@v5) are usable directly; act_runner fetches them from their origin repos.

Runner label: The registered self-hosted runner should be labeled (e.g., self-hosted or unraid). Use runs-on: self-hosted in all job definitions. Do NOT use runs-on: ubuntu-latest — that label is only resolved by GitHub's hosted runners; a Gitea self-hosted runner with ubuntu-latest label works but needs explicit configuration.

Job container image: Use container: image: catthehacker/ubuntu:act-latest for jobs that need a rich Linux environment (lint/typecheck/test). This image is the standard act runner image: includes Node.js, npm, git, curl, and system libs for Playwright. For jobs that only need Docker CLI (image build/push), no container key is needed if the runner is in Docker socket-mount mode.

MariaDB service container pattern:

jobs:
  api-integration:
    runs-on: self-hosted
    container:
      image: catthehacker/ubuntu:act-latest
    services:
      mariadb:
        image: mariadb:11
        env:
          MARIADB_ROOT_PASSWORD: testroot
          MARIADB_DATABASE: familysync_test
          MARIADB_USER: familysync
          MARIADB_PASSWORD: testpass
        options: >-
          --health-cmd="healthcheck.sh --connect --innodb_initialized"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
        working-directory: apps/api
      - run: npm run db:migrate
        working-directory: apps/api
        env:
          DB_HOST: mariadb
          DB_PORT: 3306
          DB_NAME: familysync_test
          DB_USER: familysync
          DB_PASSWORD: testpass
      - run: npm test
        working-directory: apps/api
        env:
          DB_HOST: mariadb

Critical note on MariaDB 11 health check: MariaDB 11.x Docker images removed the mysqladmin binary. The health check must use healthcheck.sh --connect --innodb_initialized (the script ships in the official image). Using mysqladmin ping will cause the service container to remain unhealthy and block the job indefinitely.

Docker build + push to Gitea container registry:

jobs:
  build-push:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ${{ vars.GITEA_REGISTRY }} # e.g. git.bergerhouse.ca
          username: ${{ secrets.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_PASSWORD }}
      - uses: docker/build-push-action@v5
        with:
          context: .
          file: apps/api/Dockerfile
          push: true
          tags: |
            ${{ vars.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/familysync-api:${{ gitea.sha }}
            ${{ vars.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/familysync-api:latest

Secrets required: REGISTRY_USER and REGISTRY_PASSWORD — a Gitea Personal Access Token with write:package scope. Gitea does NOT inject a built-in GITEA_TOKEN that grants container-registry push; a PAT is required. Store credentials in the repo's Settings > Secrets > Actions.

Docker-in-Docker consideration: If the runner is operating in Docker socket-mount mode (the default for the Gitea act_runner Docker container), the docker CLI inside a catthehacker/ubuntu:act-latest job container can reach the host Docker daemon via the mounted socket — sufficient for docker/build-push-action. If the runner is in DinD mode, additional config is needed (custom DinD image + DOCKER_HOST=tcp://docker:2376). The socket-mount mode is simpler and sufficient for this use case.

Workflow file structure recommendation:

.gitea/workflows/
  ci.yml          # lint + typecheck + vitest unit (runs on every PR push)
  integration.yml # API integration tests against MariaDB service container (runs on PR to main)
  build.yml       # Docker build + push to Gitea registry (runs on merge to main)
  mobile-test.yml # Playwright mobile tests (runs on PR to main)

Core Technologies

Technology Version Purpose Why Recommended
Node.js + TypeScript 22 LTS Backend runtime First-class typing, same language as frontend, largest CalDAV/OIDC library ecosystem
Hono 4.12.23 HTTP framework Web-Standards-native, first-class TypeScript, built-in SSE helper, WebSocket via @hono/node-server; lighter than Express and better ergonomics than Fastify for this size
Drizzle ORM 0.45.2 MariaDB query layer Type-safe SQL, zero runtime overhead, native mysql2 driver support, schema-as-code migrations via drizzle-kit
mysql2 3.22.4 MariaDB driver The only maintained native MariaDB/MySQL driver; Drizzle targets it explicitly
React 19 19.x PWA frontend Required by project; concurrent features, stable
Vite 8.0.x Build tooling De-facto standard for React PWAs; fast HMR, native ESM
vite-plugin-pwa 1.3.0 Service worker + manifest Zero-config Workbox integration, handles install prompt, offline cache, background sync scaffolding

Supporting Libraries

Library Version Purpose When to Use
tsdav 2.2.2 CalDAV client for Node.js All calendar reads and writes against Fastmail CalDAV endpoint; handles PROPFIND, REPORT, PUT, DELETE
ical.js 2.2.1 iCalendar (.ics) parsing Parse raw VCALENDAR/VEVENT payloads returned by tsdav; handles VTIMEZONE, RDATE, EXDATE
rrule 2.8.1 Recurrence rule expansion Expand RRULE strings into concrete event occurrences for the calendar view; ical.js's built-in expansion is less ergonomic for UI consumption
web-push 3.6.7 Server-side VAPID push Generate VAPID keys, sign and dispatch push messages to browser push services (APNs for iOS, FCM for Android)
@hono/oidc-auth 1.8.3 OIDC session middleware for Hono Storage-less JWT session cookies; authorization-code + PKCE flow; works with any RFC-compliant OIDC provider including Authelia
openid-client 6.8.4 Low-level OIDC primitives If @hono/oidc-auth proves insufficient (e.g., custom token introspection), use this as the lower-level escape hatch
ioredis 5.11.0 Redis client Pub/sub for broadcasting list-change events to SSE connections across Node processes
zod 3.24.x Schema validation Validate API request bodies and CalDAV event payloads before writing back to Fastmail
@hono/zod-validator 0.8.0 Hono middleware for Zod Validate request body/query in route handlers with Zod schemas
@tanstack/react-query 5.101.0 Server state + caching Manages calendar and list data fetching, background refetch, stale-while-revalidate; pairs with SSE for live list updates
zustand 5.0.14 Client state UI-only state (selected date range, color assignments, drawer open/closed); keep server state in React Query
drizzle-kit 0.31.10 Schema migrations Generates and runs MariaDB migrations from Drizzle schema definitions

Development Tools

Tool Purpose Notes
TypeScript 5.x Strict typing across backend + frontend strict: true; share types between packages via a packages/shared workspace
ESLint + Prettier Lint + format Standard config; no bikeshedding needed
Docker Compose Local dev + production parity Match Unraid stack exactly in dev
Vitest Unit + integration tests Vite-native, same config as frontend
@playwright/test v1.1 NEW — Mobile PWA test harness devDependency in apps/pwa only; 1.60.0

Installation

# Backend
npm install hono @hono/node-server @hono/oidc-auth @hono/zod-validator
npm install drizzle-orm mysql2 ioredis
npm install tsdav ical.js rrule
npm install web-push zod openid-client

# Frontend
npm install react react-dom @tanstack/react-query zustand
npm install -D vite vite-plugin-pwa

# Dev (monorepo root)
npm install -D typescript drizzle-kit vitest @types/node @types/web-push

# Dev (apps/pwa only — v1.1)
npm install -D @playwright/test
npx playwright install --with-deps chromium

Calendar Integration: CalDAV, Not JMAP

Decision: CalDAV via tsdav. JMAP for calendars is not available from Fastmail.

Fastmail's developer docs (as of 2026-06-03) state explicitly: calendar access is CalDAV only; JMAP calendar support is planned but blocked on RFC 8984 specification finalization. The JMAP working group has not finalized the calendars spec. Do not plan around JMAP calendars — it is not a near-term option.

CalDAV mechanics with tsdav:

  • Principal URL: https://caldav.fastmail.com/dav/principals/user/broker@fastmail.com/
  • tsdav performs PROPFIND on the principal to discover all calendar collections, then fetches each collection's events via REPORT (calendar-query or calendar-multiget).
  • One app password covers all calendars owned by that account under the default "Mail, Contacts & Calendars" scope.
  • tsdav returns raw iCalendar strings. Pass each to ical.js for parsing into event objects, then use rrule for RRULE expansion into the date range the UI needs.
  • Write-back (create/edit/delete): PUT a new .ics to the collection URL; DELETE by UID.

Authentication model:

Use a Fastmail app password (not OAuth) for the backend broker token. App passwords are simple HTTP Basic credentials. OAuth is intended for distributing apps to Fastmail users — not applicable here. The app password is a server secret stored in an environment variable; it never leaves the backend.

Personal calendar aggregation — IMPORTANT CAVEAT (LOW confidence):

Fastmail's multi-user calendar sharing is documented only for users within the same Fastmail account (i.e., a multi-user/family Fastmail subscription). If both household members are on the same Fastmail family plan, the primary account holder can be granted edit access to the other member's personal calendar, and the broker token for the primary account will discover and read/write those shared calendars via CalDAV. If the wife has a separate independent Fastmail account, cross-account CalDAV sharing via a single broker token is unconfirmed — this should be tested before Phase 1 commits to the personal-calendar overlay feature. The shared family calendar (owned by the primary account) works unconditionally.

What NOT to use for calendars:

  • node-ical: older fork with weaker RRULE support; ical.js is maintained by Mozilla and is the reference implementation
  • Direct fetch/axios against CalDAV: re-inventing XML namespace handling and PROPFIND parsing; tsdav exists specifically to avoid this
  • JMAP: not available for calendars on Fastmail today

Backend Framework

Decision: Hono on Node.js

Hono is the right size for this app. Express is fine but has no TypeScript-native ergonomics. NestJS is overkill for a two-person household app. Hono gives you:

  • First-class TypeScript with RPC-style type sharing (Hono RPC can export typed client for the React frontend — eliminates API drift)
  • Built-in SSE streaming helper (streamSSE) for live list updates
  • WebSocket support via @hono/node-server
  • Runs on Node.js 22 LTS in Docker with @hono/node-server

ORM: Drizzle + mysql2

Drizzle is the correct choice over Prisma for this stack:

  • Prisma generates a binary engine that adds complexity in Docker images and has weaker MariaDB compatibility signals
  • Drizzle uses mysql2 directly — the same driver you'd use raw; no runtime translation layer
  • Drizzle's mysqlTable schema is fully MariaDB-compatible (MariaDB is wire-compatible with MySQL; Drizzle's mysql dialect works)
  • Type inference from schema → query results is the core value proposition; zero runtime overhead

React PWA Stack

Build: Vite 8 + vite-plugin-pwa 1.3.0

Standard for React PWAs. vite-plugin-pwa configures the Web App Manifest and injects a Workbox service worker. Use injectManifest strategy (not generateSW) so you have explicit control over the service worker file — required for Web Push subscription management.

State/data: TanStack Query + Zustand

  • TanStack Query owns all server-side state: calendar events, lists, user profile. It handles background refetch, cache invalidation, and loading states. Use queryClient.invalidateQueries from SSE event handlers to keep list data live.
  • Zustand owns pure UI state: selected month, color assignments per calendar, drawer states. Do not put server data in Zustand.

Web Push (VAPID):

  1. Generate a VAPID key pair once (web-push generateVAPIDKeys), store in environment variables.
  2. Expose the public key via an API route; the PWA calls pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }) from a user-gesture handler.
  3. Store the PushSubscription object (endpoint, keys) in MariaDB per user.
  4. Backend sends notifications via web-push.sendNotification(subscription, payload).

iOS-specific Web Push constraints (CRITICAL):

Requirement Detail
Minimum iOS version 16.4 — push is silently unavailable on earlier versions
Installation required PWA must be added to Home Screen; push does not work from Safari browser tabs
User gesture pushManager.subscribe() must be called inside a tap handler, not on page load
EU users on iOS 17.4+ PWAs may open in Safari tabs instead of standalone mode due to DMA; affects push reach
Silent push Not supported on iOS; all push messages must display a visible notification
Background sync Not supported on iOS; no BackgroundSync or PeriodicBackgroundSync

Declarative Web Push (Safari 18.4+): Apple shipped Declarative Web Push in Safari 18.4 (iOS 18.4, March 2025). It's backward-compatible: send a JSON payload with "web_push": 8030 and the browser renders the notification without a service worker handler. The web-push npm library (v3.6.7) does not generate this format natively — you'd hand-craft the JSON payload for iOS while the same endpoint handles standard Web Push for Android/desktop. As of mid-2026, Declarative Web Push is a W3C Working Draft and the preferred format for iOS/macOS push. Build the push payload to be Declarative Web Push compatible from day one (it's just a JSON schema change), since web-push still handles the VAPID transport layer.

Onboarding UX for iOS (wife): The install-to-home-screen step is unavoidable for push notifications. Design the first-run flow to prompt this explicitly (custom install banner, step-by-step guide). Once installed, OIDC login via Authelia is one tap — the low-friction goal is achievable.


Authelia OIDC Integration

Decision: @hono/oidc-auth middleware

Authelia exposes a standards-compliant OIDC discovery endpoint. @hono/oidc-auth uses oauth4webapi under the hood, supports authorization code + PKCE, and produces storage-less JWT session cookies — no Redis or session DB required for auth state.

Flow:

  1. Unauthenticated request → middleware redirects to Authelia's authorization endpoint
  2. Authelia authenticates the user, redirects back with code
  3. Middleware exchanges code for tokens, creates signed JWT session cookie (httpOnly, Secure, SameSite=Lax)
  4. Cookie is verified on every request; refresh tokens are used to silently re-authenticate before expiry

Authelia configuration requirements:

  • response_types: [code]
  • grant_types: [authorization_code, refresh_token]
  • require_pkce: true, pkce_challenge_method: S256
  • token_endpoint_auth_method: client_secret_basic

Authelia's own integration docs show this exact pattern for Express.js (express-openid-connect). @hono/oidc-auth is the Hono-native equivalent. If it hits edge cases, openid-client v6 is the lower-level fallback.

Do NOT use: oidc-client-ts — it is a browser-side library for SPAs doing the OIDC flow in the frontend. This app has a backend session; the OIDC flow belongs on the server.


Live List Sync

Decision: SSE (Server-Sent Events) + Redis Pub/Sub, not WebSockets

Lists are co-edited by two people. The update direction is server → client (server broadcasts when one client mutates a list). SSE is simpler than WebSockets for this: plain HTTP, works through proxies, automatic reconnection in browsers.

Pattern:

  1. Client opens GET /api/lists/stream → Hono streamSSE keeps connection alive
  2. On a list mutation, the backend publishes a list:updated:{listId} event to Redis
  3. All Node processes subscribed to Redis receive the event and push it to connected SSE clients
  4. React Query on the client receives the SSE event → invalidateQueries(['lists', listId]) → refetches

Redis (ioredis) is only needed if multiple Node containers run behind a load balancer. For a single Unraid Docker Compose with one backend container, you can skip Redis and use an in-process event emitter — leave the abstraction clean so Redis can be added later.


Alternatives Considered

Recommended Alternative Why Not
Hono Express No native TypeScript ergonomics; no built-in SSE; larger ecosystem but more boilerplate
Hono Fastify Good choice but heavier plugin model; Hono's Web Standards alignment is better for this size
Drizzle Prisma Binary engine complicates Docker; weaker explicit MariaDB support; heavier
tsdav Raw fetch + xml2js CalDAV XML namespace handling is tedious; tsdav is the established TypeScript CalDAV client
ical.js node-ical node-ical is a fork that has diverged; ical.js is the Mozilla-maintained reference implementation
@hono/oidc-auth express-openid-connect express-openid-connect is Express-specific; Hono middleware is the correct fit
SSE WebSockets WebSockets are bidirectional; list sync is server→client only; SSE is simpler and proxy-friendly
CalDAV JMAP JMAP calendars not available on Fastmail as of 2026
@playwright/test playwright-cli alone playwright-cli lacks storageState save/restore and device presets needed for CI; both coexist
PAT for Gitea registry secrets.GITEA_TOKEN / built-in token Gitea does not inject a built-in token with container-registry push scope; PAT required

What NOT to Use

Avoid Why Use Instead
JMAP for calendars Not implemented by Fastmail; spec not finalized CalDAV via tsdav
Prisma Binary engine, weaker MariaDB compat, larger footprint in Docker Drizzle ORM
oidc-client-ts Browser-side OIDC library; wrong layer for a backend-session app @hono/oidc-auth
node-ical Older fork of ical.js, less maintained, weaker RRULE handling ical.js
Create React App Deprecated February 2025 Vite
PostgreSQL Not in the Unraid stack; hard constraint MariaDB
NestJS Massive framework overhead for a two-user household app Hono
Firebase/FCM as push broker Third-party dependency; VAPID direct push works without it web-push (VAPID)
mysqladmin ping health check with MariaDB 11 mysqladmin not shipped in mariadb:11 image; silently blocks CI healthcheck.sh --connect --innodb_initialized
runs-on: ubuntu-latest on Gitea self-hosted runner Label only resolves on GitHub's hosted infrastructure runs-on: self-hosted (or the runner's registered label)
Any validation library for setup wizard zod + mysql2 + web-push + Node 22 fetch cover all checks natively Use existing stack

Version Compatibility

Package Compatible With Notes
drizzle-orm@0.45.x mysql2@3.x Use drizzle-orm/mysql2 import path; mysql2@3.x uses Promises API by default
vite-plugin-pwa@1.3.x Vite@8.x, Workbox@7.x vite-plugin-pwa 0.16+ requires Node 16+; 1.x tracks Vite 6+
@hono/oidc-auth@1.8.x hono@4.x, oauth4webapi Peer-depends on hono 4.x
ical.js@2.x rrule@2.8.x Use together: ical.js parses the RRULE string, pass to new RRule(RRule.parseString(...))
web-push@3.6.x Node.js 18+ VAPID uses Web Crypto; works in Node.js 18+ natively
@playwright/test@1.60.x Node.js 18+ Install Chromium only in CI (npx playwright install --with-deps chromium)
mariadb:11 service container GitHub/Gitea Actions Health check must use healthcheck.sh; mysqladmin removed in 11.x

Open Questions Flagged for Phase Research

  1. Personal calendar cross-account sharing (LOW confidence): Does the wife's personal Fastmail calendar (if she has a separate account) appear in the broker token's CalDAV principal discovery? This must be manually tested before committing to the personal-calendar overlay in Phase 1. If it does not work, the v1 fallback is: shared family calendar only, with a read-only ICS subscription URL for the wife's personal calendar displayed separately.

  2. Declarative Web Push server-side format: The web-push npm library does not natively output the "web_push": 8030 Declarative Web Push JSON format. Verify whether iOS 18.4+ APNs endpoint accepts standard VAPID push payloads (it does for the VAPID transport layer) vs. needing the declarative JSON in the payload body. The answer is: VAPID is the transport; Declarative Web Push is the payload format. Both can coexist in the same push subscription.

  3. EU DMA regression: If either household member is in the EU on iOS 17.4+, PWA standalone mode is broken and push will not work. Confirm geographic context is outside EU — this is the project owner's constraint to verify.

  4. Gitea act_runner Docker socket access in Unraid container: The Unraid Gitea Actions runner container needs /var/run/docker.sock mounted for the Docker build job to reach the host daemon. Verify the runner container's compose config has the socket mount before the build workflow runs. Without it, docker/build-push-action will fail silently.

  5. Playwright mobile tests and DEV_AUTH_BYPASS in CI: The mobile test harness depends on DEV_AUTH_BYPASS=true being available in CI. This means the CI run starts the API with that flag — confirm it is only set in the test environment, never in the production image/deploy step.


Sources


Stack research for: FamilySync — self-hosted family calendar + shared-lists PWA on Fastmail Researched: 2026-06-03 (v1.0 baseline) / 2026-06-10 (v1.1 Operability & Polish additions)