# Phase 3: Event Write-Back + PWA Install - Context

**Gathered:** 2026-06-05
**Status:** Ready for planning

<domain>
## Phase Boundary

Members can **create, edit, and delete events** that are written back to the correct
Fastmail calendar through the existing CalDAV broker boundary (PUT / DELETE via tsdav —
never a direct Fastmail call from the UI). The app becomes an **installable PWA**
(web manifest + service worker, HTTPS) with a **guided iOS Add-to-Home-Screen walkthrough**
and Android install handling. This phase also carries the **Phase 1 Gate 2 live-auth
verification** (D-14): real Authelia OIDC login over the public Pangolin URL including the
**iOS standalone-PWA** flow, session persistence, and distinct stable per-member colors.

Requirements: CAL-04 (create timed/all-day), CAL-05 (edit), CAL-06 (delete),
CAL-07 (create recurring — whole-series only), PWA-01 (installable), PWA-02 (guided install).

**Out of scope (other phases / later):**

- Single-occurrence and "this-and-following" recurring edits (CAL-09 / CAL-10) — **v1.x**.
- Shared lists + live SSE sync (Phase 4); Web Push notifications (Phase 5).
- Wall-display / kiosk theme (v2).
- SSE as a transport — **must not be relied on in Phase 3** (unverified until Phase 4 gate, D-14).
  </domain>

<decisions>
## Implementation Decisions

### Target-calendar selection (write target)

- **D-01:** **Default target = remember last-used per member.** Seed/first-time default is
  the **creator's own personal calendar** (always exists; no shared calendar may exist yet, D-16).
- **D-02:** **Calendar picker is shown only when the member has >1 writable calendar.** With a
  single writable calendar (e.g. personal only, before a shared Family calendar exists) the
  picker is hidden entirely — zero friction for the non-technical member. It appears once a
  shared Family calendar is present.
- **D-03:** **Writable set = the member's own personal calendar + the shared Family calendar**
  (when shared read-write to them in Fastmail). The **other member's personal calendar is a
  read-only overlay** — never a write target. Matches the two-equal-partners model.
- **D-04:** **Edit may move an event to a different calendar.** Implemented as CalDAV
  **delete-from-old + create-on-new** (not an in-place move). Researcher/planner must handle
  the UID/etag implications and **partial-failure** (delete succeeded but create failed, or
  vice-versa) safely.

### Write feedback & sync (the load-bearing architecture decision)

- **D-05:** **Optimistic-accept + server-side outbox.** On save the UI optimistically reflects
  the change with a "syncing…" indicator; the API writes a **`pending` row to a MariaDB outbox**
  and returns immediately. A **backend worker drains the outbox** against Fastmail.
- **D-06:** **Re-sync on confirm.** When the queued write confirms, the worker triggers a
  **targeted re-sync of just that one calendar** (not a full poll) so the MariaDB cache becomes
  authoritative, then clears the pending state. (This is the async evolution of the operator's
  initial "forced re-sync, then show" — same authoritative-cache guarantee, without a blocking
  spinner.)
- **D-07:** **Retry policy — backoff transient, fail-fast hard errors.**
  - _Transient_ (network error, 5xx, timeout) → retry with **exponential backoff over a bounded
    window** (a few minutes), keeping the "not synced yet" toast visible.
  - _Hard_ (401/403 auth, 400 validation) → **stop immediately** and surface a real
    "didn't save" error to the user. (Transient failures must be short; anything non-transient
    is a hard fail that won't self-resolve.)
- **D-08:** **Edit-conflict handling = detect + warn + reload latest.** Writes send `If-Match`
  with the cached etag. On **412 Precondition Failed**, the write is **routed out of the retry
  loop** into a conflict flow: re-sync that event from Fastmail and warn the user
  ("this event changed elsewhere — review the latest version") before they retry. **No silent
  last-write-wins overwrite.**
- **D-09:** **Sync-state is surfaced via polling, not SSE.** The "pending / not-synced" state the
  toast reads must be exposed over a polled endpoint (or query refetch), because SSE-over-Pangolin
  is unverified until the Phase 4 entry gate (D-14). Do **not** build Phase 3 sync feedback on SSE.

### Carried forward — locked, NOT re-discussed

- **D-10:** The **edit/delete surface reuses `EventDetailPopover`** — it was built in Phase 2 with
  a reserved footer action area specifically for this (Phase 2 D-08). Create can reuse the same
  surface shell.
- **D-11:** **Recurring events: create + whole-series edit only** in v1. Single-occurrence
  (RECURRENCE-ID/EXDATE) and "this-and-following" edits are **v1.x** (CAL-09/CAL-10).
- **D-12:** **Broker is the only Fastmail I/O boundary.** Routes touch only the MariaDB cache;
  all PUT/DELETE goes through `src/broker/`. No tsdav import in route handlers.
- **D-13:** **Dev-auth bypass** stays available for local build/test (project D-14); live
  Authelia verification is the Gate 2 item folded into this phase.

### Claude's Discretion (researcher / planner decide)

- **Event form & fields** — exact field set (title, start/end, all-day toggle, location,
  description) and layout. Build it into / alongside the reused `EventDetailPopover` surface (D-10).
  Must be slick and low-friction for the non-technical Apple member.
- **Recurrence creation UX** — how whole-series RRULE is exposed (simple presets daily/weekly/
  monthly/yearly vs a small custom builder). Keep it minimal for v1; whole-series only (D-11).
- **iOS install onboarding** — trigger (auto-detect iOS-Safari-non-standalone vs help button vs
  first-visit banner) and the annotated Add-to-Home-Screen walkthrough content. **Load-bearing:**
  success criterion 4 requires a non-technical user to follow it independently; no install ⇒ no
  push in Phase 5.
- **Android install** — `beforeinstallprompt` handling (custom button vs native prompt).
- **PWA tooling** — `vite-plugin-pwa` is in the recommended stack (CLAUDE.md) but **not yet
  installed**; manifest + service worker config is the planner's call. Keep the service worker
  conservative (don't break the OIDC redirect / standalone-PWA login flow — Gate 2 risk).
- Outbox worker mechanics (interval vs trigger, idempotency key, max-attempt count, dead-letter
  surfacing) — implement to satisfy D-05/D-06/D-07.

</decisions>

<canonical_refs>

## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Project decisions & scope

- `.planning/PROJECT.md` — core value, constraints, Key Decisions incl. **D-14** (dev-auth
  bypass; live AUTH + iOS smoke folded into Phase 3), **D-15** (local Newt test rig for Gate 2),
  **D-16** (shared Family calendar is a collection on the operator's primary account, may not
  exist yet; `is_shared` flag marks it).
- `.planning/ROADMAP.md` §"Phase 3: Event Write-Back + PWA Install" — goal + the **6 success
  criteria** (incl. criterion 6, the carried Gate 2 live-auth/iOS verification).
- `.planning/REQUIREMENTS.md` — CAL-04/05/06 (create/edit/delete), CAL-07 (recurring create),
  PWA-01/02 (installable + guided install); v1.x CAL-09/10 (single-occurrence edits — OUT).

### Phase 1/2 foundation this builds on

- `.planning/phases/02-calendar-display/02-CONTEXT.md` — design-token layer (D-01/02/03),
  `EventDetailPopover` reuse-as-edit-surface (Phase 2 D-08), server-side expansion, color/owner model.
- `.planning/phases/01-foundation-broker-spike/CAL-08-DECISION.md` — per-member app-password
  model (how each member's calendars are reached/written; informs the writable set, D-03).
- `docs/deployment.md` — **Gate 2 checklist** (live Authelia OIDC over Pangolin, iOS standalone
  PWA) and the dev-auth bypass context. **Required reading for success criterion 6.**

### Code this phase extends

- `apps/api/src/broker/client.ts` — tsdav `createDAVClient`; add PUT/DELETE write methods here
  (broker boundary, D-12).
- `apps/api/src/broker/sync.ts` — REPORT→ical.js→upsert; the targeted single-calendar re-sync
  (D-06) builds on this.
- `apps/api/src/broker/poller.ts` — ctag poller; the outbox worker is a sibling background process.
- `apps/api/src/routes/events.ts` — current read-only `GET /api/events`; add create/edit/delete
  endpoints + the pending/sync-state surface (D-09) alongside.
- `apps/api/src/db/schema.ts` — `calendars` (userId, isShared, url), `calendarEvents`
  (uid, etag, rawVevent, dtstart split). **New outbox table** lives here (D-05).
- `apps/pwa/src/components/EventDetailPopover.tsx` — reserved footer action area is the edit/delete
  entry point (D-10).
- `apps/pwa/src/api/client.ts` — typed fetch client to extend with write calls + sync-state poll.
- `apps/pwa/vite.config.ts` — no PWA plugin yet; manifest + service worker added here (PWA-01).
- `CLAUDE.md` — locked stack incl. `vite-plugin-pwa` 1.3.0, tsdav write-back guidance
  (PUT new .ics / DELETE by UID), iOS PWA constraints (16.4+, home-screen install required).

</canonical_refs>

<code_context>

## Existing Code Insights

### Reusable Assets

- `EventDetailPopover` (Phase 2) — read-only detail overlay with a **reserved footer for Phase 3
  edit/delete** (D-10); responsive bottom-sheet/popover, focus trap, XSS-safe plain-text rendering.
- `apps/pwa/src/api/client.ts` — typed `fetch` client (`fetchMe`, windowed `fetchEvents`,
  `credentials: 'include'`); extend with create/edit/delete + sync-state poll.
- Broker module (`client.ts`/`sync.ts`/`poller.ts`) — owns all Fastmail I/O and ical.js parsing;
  write methods and the outbox worker attach here.
- Design-token layer + Zustand UI store + TanStack Query — server state in Query, UI state in Zustand.

### Established Patterns

- **Broker boundary (T-03-02):** routes read the MariaDB cache only; never import tsdav in a route.
  Write-back must keep this — endpoint enqueues to the outbox; the broker worker does the CalDAV PUT.
- **D-13 schema split** for all-day (dtstartDate) vs timed (dtstartUtc) — write-back must produce
  correct VEVENTs for both and never coerce DATE→DATETIME.
- **Idempotency:** `calendar_id + uid` composite unique key; etag tracked per event (drives D-08).
- Hono app exported without auto-start (testable); add write routes + outbox worker in that shape.

### Integration Points

- **New MariaDB outbox table** (D-05): pending create/edit/delete operations with status, attempt
  count, target calendar URL, payload/UID, etag for If-Match.
- **Outbox worker** (sibling to the ctag poller) drains the queue, applies D-07 retry/backoff,
  triggers the targeted re-sync (D-06), and updates pending status read by the polled sync-state
  endpoint (D-09).
- **Write endpoints** on the events router (create/edit/delete) that validate input (zod) and
  enqueue rather than calling Fastmail inline.
- **PWA layer**: `vite-plugin-pwa` manifest + service worker (PWA-01) — keep the SW conservative
  so it does not break the OIDC redirect / iOS standalone login (Gate 2, success criterion 6).

</code_context>

<specifics>
## Specific Ideas

- The non-technical Apple member is the design center: the calendar picker disappears when there's
  only one choice (D-02), saves never block on a spinner (D-05/D-06), and a failed sync shows a
  clear, persistent "not synced yet" state rather than silently losing the edit.
- "Transient failures should be short; anything beyond that is a hard fail that won't auto-resolve"
  — drives the bounded-backoff-then-fail policy (D-07).
- iOS install walkthrough must be followable independently with annotated screenshots — it's the
  prerequisite for her getting Web Push in Phase 5.

</specifics>

<deferred>
## Deferred Ideas

- **Single-occurrence / "this-and-following" recurring edits** (CAL-09/CAL-10) — v1.x; Phase 3 is
  create + whole-series edit only.
- **Writing to the other member's personal calendar** — out; other members' personals are
  read-only overlays in v1 (D-03).
- **SSE-based live sync-state push** — deferred to after the Phase 4 SSE-over-Pangolin gate; Phase 3
  surfaces sync state via polling (D-09).
- **Event form areas not deep-dived** (exact fields, recurrence-builder richness, iOS/Android
  install UX) — left to researcher/planner discretion within the constraints above; not deferred
  out of phase, just not operator-locked.

</deferred>

---

_Phase: 3-Event Write-Back + PWA Install_
_Context gathered: 2026-06-05_
