# Hosted Capture Pipeline — Email + Voice + Push

> Status: design. Target: ADR-0029.

This is the hosted-only continuation of Phase 10. Local self-host
already ships the synchronous `POST /api/inbox` endpoint
(ADR-0024). What this doc covers: inbound email, voice
transcription, and web push notifications — all of which need
hosted infrastructure.

## Inbound email (Postmark)

### Address shape

Each tenant gets `inbox+<slug>@in.Lycato`. Configurable forwarder
so founder can use a friendlier address (`me@founder.com → +<slug>@in.Lycato`).

### Webhook

- Postmark inbound parse posts to
  `https://control.Lycato/webhook/postmark/<tenant_id>?token=...`.
- Token rotates monthly; mismatch returns 401.
- Webhook handler:
  1. Validates Postmark signature.
  2. Pulls subject, body (text part preferred), attachments.
  3. Calls the tenant container's `POST /api/inbox` with:
     `{ title: subject, body: text, source: "email" }`.
  4. Saves attachments (PDFs, etc.) into `inbox/attachments/<id>/`.
- 5 MB total per-email cap. Larger → bounce with explanation.

### Reply-by-email

Out of scope for v0.4.0. Reply path is "founder edits in cockpit."

### Spam

Postmark's built-in spam filter + sender allowlist on the tenant
side (set in Settings).

## Voice capture (Whisper)

### Flow

1. PWA "record" button → captures audio as `audio/webm` (Opus).
2. POSTs to `/api/inbox/voice` with the audio blob (≤2 minutes,
   ≤5 MB).
3. Server stores raw audio under
   `inbox/attachments/<id>/audio.webm`.
4. Server calls Whisper (BYO OpenAI key from the tenant secret
   store) with the audio.
5. Transcript written to `inbox/<id>.md` as the body. Title is
   the first sentence (capped 80 chars).
6. Original audio retained for 30 days then deleted (founder can
   opt-in to keep longer).

### Why Whisper not local

Local Whisper.cpp would require shipping a 200MB+ model per
container. Cost asymmetry: a paid Whisper API call is ~$0.006/min
of audio; voice capture is rare; the per-tenant amortised cost is
negligible.

### Cost guardrails

- Tenant-side per-day cap: 60 minutes default.
- Tenant-side per-month cap: 1000 minutes default.
- Both raise/lower in Settings; both have a hard ceiling at 10x
  default to protect runaway scripts.

## Web push notifications

### Use cases

1. High-risk approval pending for >2 hours.
2. Daily-digest agent failed during scheduled run.
3. Cofounder approved a branch (J2 closes faster).

### Stack

- Web Push API (RFC 8030) — browser-agnostic, no Firebase / APNs
  bridge needed for the alpha.
- VAPID keys per tenant; subscription endpoint stored alongside
  the user session.
- Payloads are tiny: `{ kind, title, url }`. The actual content
  lives in the cockpit, never in the push.

### Opt-in

Per-user, per-kind. Founder can subscribe to "approval pending" but
mute "daily-digest failed" if it's noisy. Default = all off; opt-in
on first cockpit visit after enabling notifications.

### Privacy

No content in payloads. Subscription endpoints are encrypted with
the tenant DEK (treated as a key-equivalent because they can be
abused to send push to the founder's device).

## Local self-host

Self-host installs do NOT get any of this. Workarounds:

- Email: any IMAP-poller can land emails as files in
  `inbox/` — write a tiny adapter.
- Voice: use `lycato inbox voice <file>` (future) — a local Whisper
  call via BYO key.
- Push: use macOS Shortcut + the local `POST /api/inbox` endpoint.

Self-host parity for these is **not** a v0.4.0 goal; if a self-host
founder pushes for it, the SDK already provides the surface to
build it.

## What's intentionally absent

- SMS capture. Twilio cost / spam risk not worth it for the
  segment.
- Multi-language voice. Whisper handles many languages OK; we
  don't add per-language tuning in v0.4.0.
- Native iOS / Android apps. PWA covers 90% of capture; native
  enters scope post v1.0.
