---
phase: 03-event-write-back-pwa-install
plan: 03
subsystem: api
tags: [hono, drizzle, zod, calendarOutbox, write-back, outbox-pattern, access-control, tdd]

requires:
  - phase: 03-event-write-back-pwa-install/03-01
    provides: calendarOutbox schema + calendarEvents.objectUrl + Wave-0 RED test scaffold
  - phase: 03-event-write-back-pwa-install/03-02
    provides: broker primitives (vevent.ts, write.ts) — not used by routes but confirm broker boundary

provides:
  - POST /api/events/create — validates, checks D-03 ownership, enqueues pending outbox row, returns 202 with uid
  - PATCH /api/events/:uid/edit — looks up event, checks ownership, enqueues update or transaction-paired delete+create for calendar moves
  - DELETE /api/events/:uid — looks up event, checks ownership, enqueues delete row with server-side etag
  - GET /api/events/sync-status — member-scoped outbox status poll (D-09)
  - GET /api/events/writable-calendars — authoritative D-03 writable set (own personal + shared Family; never other member's personal)
  - zod schemas for event fields (title 255, location/description 2000 — T-03-08 bounds)

affects:
  - 03-04 (outbox worker drains rows these endpoints enqueue)
  - 03-05 (EventForm + client.ts consume these endpoints + writable-calendars)

tech-stack:
  added: []
  patterns:
    - "resolveUserId(c): dev-bypass c.get('user') first, fallback to getAuth(c) for OIDC — same pattern as me.ts"
    - 'Enqueue-only write endpoints: no Fastmail call in routes; db.insert(calendarOutbox) is the only side effect'
    - 'Edit-as-move: db.transaction with paired delete+create sharing a groupId (D-04)'
    - 'sync-status: .orderBy(desc(createdAt)).limit(1) to get latest outbox row; userId-scoped (T-03-07)'
    - 'writable-calendars: WHERE userId=currentUser.id OR isShared=1 — authoritative D-03 enforcement (T-03-11)'
    - 'Test mock pattern for db.transaction: factory fn cb receives mock tx with insert; vi.mock hoisted factory captures mutable refs'
    - "devAuthBypass mock in tests: vi.mock('../auth/devBypass.js') injects dev user so write tests get authenticated context"

key-files:
  created: []
  modified:
    - apps/api/src/routes/events.ts
    - apps/api/tests/routes/events.test.ts

key-decisions:
  - 'resolveUserId helper uses any type to avoid Hono context generic complexity — acceptable for internal helper'
  - 'Two-query ownership check for edit/delete (get event, then check calendar isShared) to maintain simple from().where() chain that test mocks can intercept without innerJoin complexity'
  - 'Writable-calendars response maps to { url, displayName, color, isShared } — the Plan 05 WritableCalendar shape'
  - "sync-status returns { uid, status: 'done' } when no outbox row found (nothing pending = settled)"

patterns-established:
  - 'Enqueue-only write route: validate → check ownership → db.insert(calendarOutbox) → return 202; no broker call'
  - 'D-03 ownership enforcement at two layers: write endpoints AND writable-calendars listing'
  - 'vi.mock devAuthBypass for write-endpoint tests avoids needing ENV manipulation or OIDC infrastructure'

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

duration: 7min
completed: 2026-06-05
---

# Phase 03 Plan 03: Write API Surface Summary

**Hono write endpoints (create/edit/delete + sync-status + writable-calendars) enqueue to calendarOutbox with D-03 ownership enforcement; zod-validated, 202 optimistic-accept, no Fastmail call**

## Performance

- **Duration:** ~7 min
- **Started:** 2026-06-05T17:51:00Z
- **Completed:** 2026-06-05T21:58:08Z
- **Tasks:** 3 (Tasks 1-2-3 implemented in one feat commit; TDD RED gate committed separately)
- **Files modified:** 2

## Accomplishments

- All five write/status/writable-calendars endpoints enqueue-only and member-scoped
- D-03 ownership enforced on both the write path and the writable-calendars listing
- D-04 edit-as-move pair implemented transactionally (db.transaction with shared groupId)
- D-09 polling endpoint (sync-status) live with strict userId scoping (T-03-07)
- Broker boundary preserved: no tsdav import in routes/events.ts

## Task Commits

1. **RED gate** — `e14c5da` (test): extend events tests — write/sync-status/writable-calendars endpoints (9 new failing tests)
2. **GREEN + Tasks 1/2/3** — `0a82223` (feat): implement write API surface — all 69 events tests GREEN, tsc clean

## Files Created/Modified

- `apps/api/src/routes/events.ts` — extended with POST /create, PATCH /:uid/edit, DELETE /:uid, GET /sync-status, GET /writable-calendars; auth helper; zod schemas; `db.transaction` for edit-as-move
- `apps/api/tests/routes/events.test.ts` — extended with 9 new write-endpoint tests; wired db.insert + db.transaction into vi.mock; added devAuthBypass mock for auth injection

## Decisions Made

- **resolveUserId uses `any` type:** Hono's generic context type is complex to thread through a standalone helper; `any` is acceptable for an internal module-private helper that does a simple property access.
- **Two-query ownership check for edit/delete:** Rather than innerJoin (which would break the flat from().where() mock chain in tests), the implementation does a second query on calendars to check isShared when the event's userId doesn't match. Both queries share the same mock chain in tests, which works because both return the seeded mockDbRows.
- **writable-calendars response shape:** `{ url, displayName, color, isShared }` matches the `WritableCalendar` shape Plan 05's `fetchWritableCalendars` expects.
- **sync-status default to 'done':** When no outbox row exists for a UID, the endpoint returns `{ uid, status: 'done' }` — nothing pending means the event is settled.

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] TypeScript error on resolveUserId helper**

- **Found during:** Task 1 (implementation) — tsc --noEmit reported TS2493/TS2339 on complex Hono context type inference
- **Issue:** The helper function tried to infer the Hono context type from `eventsRouter.get` parameters, which failed due to tuple type length mismatch
- **Fix:** Changed helper parameter to `any` with inline cast; added clarifying comment
- **Files modified:** apps/api/src/routes/events.ts
- **Verification:** `tsc --noEmit` passes clean
- **Committed in:** 0a82223

---

**Total deviations:** 1 auto-fixed (Rule 1 - type error)
**Impact on plan:** Minor typing accommodation; no behavior change.

## Issues Encountered

- Test mock architecture required careful design: the existing `vi.mock` for db/client.js only mocked `db.select`; extending it to include `db.insert` and `db.transaction` required restructuring the mock factory to use mutable `vi.fn()` references that can be reassigned in `beforeEach`. The devAuthBypass mock was added to give write-endpoint tests an authenticated user context without ENV manipulation.

## Known Stubs

None — all endpoints are fully wired to the DB schema. The outbox rows they insert will be drained by the Plan 04 worker; until that plan runs, rows accumulate in pending state (correct behavior).

## Threat Flags

No new network endpoints or auth paths beyond what is in the plan's threat model. All T-03-06 through T-03-11 mitigations are implemented.

## Self-Check

- [x] `apps/api/src/routes/events.ts` exists and includes all 5 endpoints
- [x] `apps/api/tests/routes/events.test.ts` exists and tests are GREEN (69 passed)
- [x] Commits e14c5da (test RED) and 0a82223 (feat GREEN) exist
- [x] `grep -c "tsdav\|createFastmailClient" apps/api/src/routes/events.ts` = 1 (comment only, not import)
- [x] `grep -c "db.transaction" apps/api/src/routes/events.ts` = 1
- [x] tsc --noEmit passes clean

## Self-Check: PASSED

## Next Phase Readiness

- Plan 04 (outbox worker): `calendarOutbox` rows are being enqueued; worker can now drain them
- Plan 05 (EventForm + client.ts): POST /create, PATCH /:uid/edit, DELETE /:uid endpoints are live; GET /writable-calendars provides the picker data

---

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