---
phase: 03-event-write-back-pwa-install
plan: 01
subsystem: database, testing, infra
tags: [drizzle, mariadb, vitest, vite-plugin-pwa, caldav, outbox]

# Dependency graph
requires:
  - phase: 02-calendar-read-display
    provides: calendarEvents table, sync.ts upsert loop, existing test infrastructure

provides:
  - calendarOutbox table live in MariaDB (calendar_outbox, 3 indexes)
  - calendarEvents.objectUrl column live in MariaDB (object_url varchar 1024)
  - vite-plugin-pwa installed in apps/pwa
  - Five Wave 0 RED test files covering all Phase 3 behaviors (vevent, write, outboxWorker, events routes, InstallPrompt)

affects: [03-02, 03-03, 03-04, 03-05, 03-06, 03-07, 03-08]

# Tech tracking
tech-stack:
  added: [vite-plugin-pwa@1.3.0]
  patterns:
    - mysqlEnum for outbox status/operation columns in Drizzle schema
    - objectUrl stored on calendarEvents from obj.url during sync upsert
    - Wave 0 RED scaffold: import not-yet-existing modules so test suite fails before implementation

key-files:
  created:
    - apps/api/tests/broker/vevent.test.ts
    - apps/api/tests/broker/write.test.ts
    - apps/api/tests/broker/outboxWorker.test.ts
    - apps/pwa/src/components/InstallPrompt.test.tsx
  modified:
    - apps/api/src/db/schema.ts
    - apps/api/src/broker/sync.ts
    - apps/api/tests/routes/events.test.ts
    - apps/pwa/package.json

key-decisions:
  - 'D-Task5-DDL: drizzle-kit push is unsafe on MariaDB 11 with mysql dialect — misreads metadata and schedules truncate on populated tables. Additive DDL (calendar_outbox CREATE + object_url ALTER) was hand-applied and verified. Adopt drizzle-kit generate+migrate workflow before next schema change (tracked in todos/pending/adopt-drizzle-migrations-workflow.md).'

patterns-established:
  - 'Outbox pattern: calendar_outbox table with status enum (pending/done/failed/dead), groupId for edit-as-move pairing, nextAttemptAt for exponential backoff'
  - 'objectUrl stored from tsdav obj.url on every sync upsert — enables If-Match header on CalDAV update/delete'
  - 'Wave 0 RED scaffold: all phase test files created before any implementation so GREEN gate is explicit'

requirements-completed: [CAL-04, CAL-05, CAL-06, CAL-07, PWA-01, PWA-02]

# Metrics
duration: ~45min
completed: 2026-06-05
---

# Phase 03 Plan 01: Foundation Scaffold Summary

**calendarOutbox table + calendarEvents.objectUrl pushed live to MariaDB, vite-plugin-pwa installed, and five Wave 0 RED test files covering all Phase 3 write-back and PWA behaviors**

## Performance

- **Duration:** ~45 min
- **Started:** 2026-06-05T21:18Z
- **Completed:** 2026-06-05T22:10Z
- **Tasks:** 5 (Tasks 1-5; Task 1 was human-verify gate, Task 5 was human-action gate)
- **Files modified:** 8

## Accomplishments

- Extended Drizzle schema with `calendarOutbox` table (12 columns, 3 indexes: idx_outbox_user_status, idx_outbox_next_attempt, idx_outbox_uid) and `calendarEvents.objectUrl` column; both live in MariaDB
- Populated `objectUrl: obj.url ?? null` in both `.values()` and `.onDuplicateKeyUpdate()` blocks of the calendarEvents upsert in sync.ts — enables If-Match writes (D-08)
- Installed `vite-plugin-pwa` (legitimacy-gated via Task 1 supply-chain checkpoint T-03-SC)
- Created five Wave 0 RED test files covering every Phase 3 behavior: VEVENT builder, CalDAV write layer, outbox state machine, events API routes, and PWA InstallPrompt

## Task Commits

1. **Task 1: Supply-chain gate T-03-SC** — no commit (verification-only checkpoint)
2. **Task 2: Extend Drizzle schema + install vite-plugin-pwa** — `78f0dee` (feat)
3. **Task 3: Populate calendarEvents.objectUrl in sync.ts** — `0c0bcef` (feat)
4. **Task 4: Wave 0 RED test scaffold** — `bbfccda` (test)
5. **Task 5: Push schema to MariaDB** — hand-applied DDL by orchestrator (no code commit; DB verified)

## Files Created/Modified

- `apps/api/src/db/schema.ts` — added mysqlEnum import, calendarOutbox table definition, groupId column, 3 indexes; added objectUrl column to calendarEvents
- `apps/api/src/broker/sync.ts` — set `objectUrl: obj.url ?? null` in values and onDuplicateKeyUpdate blocks
- `apps/api/tests/broker/vevent.test.ts` — RED: VCALENDAR/VEVENT builder tests (timed, all-day D-13, RRULE)
- `apps/api/tests/broker/write.test.ts` — RED: createCalendarEvent, updateCalendarEvent (If-Match), deleteCalendarEvent
- `apps/api/tests/broker/outboxWorker.test.ts` — RED: outbox state machine (pending→done/failed/backoff/dead), edit-as-move ordering (D-04)
- `apps/api/tests/routes/events.test.ts` — extended with POST create, PATCH edit, DELETE, sync-status, writable-calendars, D-03 access control
- `apps/pwa/src/components/InstallPrompt.test.tsx` — RED: isIOSSafariNonStandalone(), useAndroidInstallPrompt
- `apps/pwa/package.json` — added vite-plugin-pwa dependency

## Decisions Made

- **D-Task5-DDL:** `drizzle-kit push` with the `mysql` dialect against a live MariaDB 11 instance produces a FALSE destructive diff — it misreads MariaDB-11 metadata and schedules `truncate table` on `calendars`, `calendar_events`, and `users` (503 events at risk). The two genuinely additive statements were hand-applied by the orchestrator and verified. A follow-up todo (`.planning/todos/pending/adopt-drizzle-migrations-workflow.md`) tracks migrating to `drizzle-kit generate` + `drizzle-kit migrate` before any future schema change. No `drizzle-kit push` should be run against this instance again.

## Deviations from Plan

### Task 5: drizzle-kit push replaced by hand-applied additive DDL

**Category:** Orchestrator-resolved deviation (not a Rule 1–4 auto-fix; resolved by human operator per gate instructions)

- **Found during:** Task 5 (blocking human-action gate)
- **Issue:** `drizzle-kit push` with the Drizzle `mysql` dialect against MariaDB 11 misread database metadata and reported a destructive plan including `truncate table` on `calendars`, `calendar_events`, and `users`. This is a known incompatibility — drizzle-kit 0.31.10 has no `mariadb` dialect; the `mysql` dialect misinterprets MariaDB-11 server metadata.
- **Fix:** Orchestrator manually ran only the two additive statements: `CREATE TABLE calendar_outbox (...)` matching schema.ts exactly, and `ALTER TABLE calendar_events ADD COLUMN object_url varchar(1024)`. Data verified intact (calendars=1, calendar_events=503).
- **Files modified:** None (DB DDL only; schema.ts was already correct)
- **Verification:** `SHOW TABLES LIKE 'calendar_outbox'` → 1 row; `SHOW COLUMNS FROM calendar_events LIKE 'object_url'` → 1 row
- **Follow-up:** `.planning/todos/pending/adopt-drizzle-migrations-workflow.md` created to track migrating to generate+migrate workflow

---

**Total deviations:** 1 (Task 5 DDL approach replaced; resolved by operator at the blocking gate)
**Impact on plan:** No scope creep. Schema is correct. Must-haves fully satisfied. Follow-up todo prevents recurrence.

## Issues Encountered

None beyond the Task 5 drizzle-kit deviation documented above.

## User Setup Required

None — no external service configuration required for this plan. The schema push was a one-time operation handled by the orchestrator at the Task 5 gate.

## Next Phase Readiness

- Wave 0 RED scaffold is in place — plans 03-02 through 03-08 can proceed to GREEN implementation
- `calendarOutbox` and `calendarEvents.objectUrl` are live; outbox worker and write routes can reference them immediately
- `vite-plugin-pwa` is installed; PWA manifest configuration (Plan 03-06) can proceed
- **Action before next schema change:** Adopt `drizzle-kit generate` + `drizzle-kit migrate` (see pending todo) — do NOT run `drizzle-kit push` again

---

_Phase: 03-event-write-back-pwa-install_
_Completed: 2026-06-05_
