docs: v1.1 research (stack/features/architecture/pitfalls/summary)
This commit is contained in:
+189
-4
@@ -1,12 +1,171 @@
|
||||
# Stack Research
|
||||
|
||||
**Domain:** Self-hosted family calendar + shared-lists PWA on Fastmail
|
||||
**Researched:** 2026-06-03
|
||||
**Researched:** 2026-06-03 (v1.0) / 2026-06-10 (v1.1 additions)
|
||||
**Confidence:** MEDIUM-HIGH (calendar sharing cross-account caveat: LOW; rest HIGH)
|
||||
|
||||
---
|
||||
|
||||
## Recommended Stack
|
||||
## 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:**
|
||||
|
||||
```typescript
|
||||
// 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:**
|
||||
|
||||
```yaml
|
||||
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:**
|
||||
|
||||
```yaml
|
||||
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)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommended Stack (v1.0 baseline — unchanged)
|
||||
|
||||
### Core Technologies
|
||||
|
||||
@@ -45,6 +204,7 @@
|
||||
| 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** |
|
||||
|
||||
---
|
||||
|
||||
@@ -61,8 +221,12 @@ npm install web-push zod openid-client
|
||||
npm install react react-dom @tanstack/react-query zustand
|
||||
npm install -D vite vite-plugin-pwa
|
||||
|
||||
# Dev
|
||||
# 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
|
||||
```
|
||||
|
||||
---
|
||||
@@ -204,6 +368,8 @@ Redis (`ioredis`) is only needed if multiple Node containers run behind a load b
|
||||
| @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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -219,6 +385,9 @@ Redis (`ioredis`) is only needed if multiple Node containers run behind a load b
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -231,6 +400,8 @@ Redis (`ioredis`) is only needed if multiple Node containers run behind a load b
|
||||
| @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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -242,6 +413,10 @@ Redis (`ioredis`) is only needed if multiple Node containers run behind a load b
|
||||
|
||||
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
|
||||
@@ -261,8 +436,18 @@ Redis (`ioredis`) is only needed if multiple Node containers run behind a load b
|
||||
- [PWA iOS Limitations 2026](https://www.magicbell.com/blog/pwa-ios-limitations-safari-support-complete-guide) — iOS 16.4 minimum; home screen required; EU DMA regression
|
||||
- [Authelia Express.js Integration](https://www.authelia.com/integration/openid-connect/clients/expressjs/) — Authorization code + PKCE flow; client_secret_basic
|
||||
- [@hono/oidc-auth GitHub](https://github.com/honojs/middleware/tree/main/packages/oidc-auth) — Storage-less JWT session cookies; Version 1.8.3
|
||||
- [@playwright/test npm](https://www.npmjs.com/package/@playwright/test) — Version 1.60.0 current; storageState, devices registry confirmed
|
||||
- [Playwright Authentication docs](https://playwright.dev/docs/auth) — storageState save/restore pattern, worker-scoped fixture
|
||||
- [Playwright Emulation docs](https://playwright.dev/docs/emulation) — devices['iPhone 15 Pro'], isMobile, viewport, userAgent presets
|
||||
- [Gitea Container Registry docs](https://docs.gitea.com/usage/packages/container) — Registry URL format, PAT required for push
|
||||
- [Automating Docker builds with Gitea Actions](https://www.vanmeeuwen.dev/blog/automating-docker-builds-with-gitea-actions) — docker/login-action@v3 + docker/build-push-action workflow pattern
|
||||
- [Gitea Official Tutorial — Automating Release Versioning](https://about.gitea.com/resources/tutorials/automating-release-versioning-with-gitea-actions-to-the-gitea-package-registry) — Complete workflow YAML with docker/setup-buildx-action, registry secrets
|
||||
- [Gitea runner-images](https://gitea.com/gitea/runner-images) — catthehacker/ubuntu:act-latest as recommended job container
|
||||
- [MariaDB 11 health check fix](https://github.com/mage-os/github-actions/issues/365) — mysqladmin removed in mariadb:11.4; healthcheck.sh required
|
||||
- [MySQL in GitHub Actions (ovirium.com)](https://ovirium.com/blog/how-to-make-mysql-work-in-your-github-actions/) — Service container pattern; ports, env, options syntax (GitHub-compatible = Gitea-compatible)
|
||||
- [DEV Community — Docker-in-Docker with Gitea Actions](https://dev.to/tmlr/the-definitive-guide-to-safe-docker-in-docker-with-gitea-actions-331l) — DinD vs socket-mount tradeoffs; socket-mount recommended for homelab
|
||||
|
||||
---
|
||||
|
||||
*Stack research for: FamilySync — self-hosted family calendar + shared-lists PWA on Fastmail*
|
||||
*Researched: 2026-06-03*
|
||||
*Researched: 2026-06-03 (v1.0 baseline) / 2026-06-10 (v1.1 Operability & Polish additions)*
|
||||
|
||||
Reference in New Issue
Block a user