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 ([email protected] → +<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:
- Validates Postmark signature.
- Pulls subject, body (text part preferred), attachments.
- Calls the tenant container's
POST /api/inboxwith:{ title: subject, body: text, source: "email" }. - 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
- PWA "record" button → captures audio as
audio/webm(Opus). - POSTs to
/api/inbox/voicewith the audio blob (≤2 minutes, ≤5 MB). - Server stores raw audio under
inbox/attachments/<id>/audio.webm. - Server calls Whisper (BYO OpenAI key from the tenant secret store) with the audio.
- Transcript written to
inbox/<id>.mdas the body. Title is the first sentence (capped 80 chars). - 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
- High-risk approval pending for >2 hours.
- Daily-digest agent failed during scheduled run.
- 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/inboxendpoint.
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.