QueuePort (constitution category 7)
- Interface in packages/core:
publish(queue, payload, opts),schedule(queue, payload, runAt). Payloads are Zod-validated envelopes:{ job_id: uuidv7, tenant_id, correlation_id, kind, data }. - Binding per module composition root (R21). F1 default adapter: Vercel Queues. New vendors (QStash, etc.): ADR before first import.
Consumers
- Idempotency MANDATORY: check/insert into
core.processed_jobs (job_id, consumer, processed_at)inside the work transaction; duplicate ⇒ ack and skip. TTL cleanup job. - Retry policy (industry default, override per queue with rationale in module spec): 5 attempts, exponential backoff with jitter (base 30s, cap 1h).
- Exhausted retries and hard failures MUST persist to
core.dead_letters (job_id, queue, payload, attempts, last_error, status: pending_review|replayed|discarded, correlation_id)+ BetterStack alert. Replay tooling lives in Softcrum Ops. High-volume successes are metrics, not rows. - Poison messages: schema-invalid payloads go straight to dead_letters (no retries).
Notification queues (with standards/notifications.md)
- One queue per channel per rail:
notif.{channel}.transactionalandnotif.{channel}.marketing. - Transactional ALWAYS has priority: marketing workers yield (pause/drain) when transactional depth > threshold.
- Rate limiting: token bucket in Upstash per PROVIDER (global — e.g., Resend 10 rps is per team across ALL tenants) and per TENANT (fair use). 429 ⇒ backoff, never drop.
- Email blasts: Resend batch endpoint (100/request) exclusively; NEVER sync contacts to Resend Audiences (our store is the source of truth).
Cron fan-out
- Per-tenant fan-out pattern (existing) for: partition pre-creation, retention export+purge, nightly reconciliation, data-freshness recomputes (tier per entitlement: nightly/12h/6h/3h/1h), points_expiring_soon scans, usage snapshots (30 min).