App Control Manifest

Operational overview of every running component in Eko. Drop this file into a Claude Code session to inspect, diagnose, or control app functions.

How it works: Each section catalogs a category of operational component with its current status, schedule, dependencies, and env-var controls. YAML blocks are machine-readable directives — a Claude session or admin UI can parse them to render dashboards, trigger actions, or surface health checks.


Deployment Status

deployment:
  platform: vercel
  project: eko-web
  team: lab-90
  production_alias: app.eko.day        # Crons ONLY run on production
  preview_alias: eko-web-git-dev-lab-90.vercel.app
  current_environment: preview          # No production deploy active
  crons_running: false                  # Crons require production deployment

Note: Vercel crons only execute against the production deployment. All current deployments target preview, so no crons are firing.


1. Scheduled Tasks (Cron Jobs)

1a. Scheduled in vercel.json (5)

These crons are defined in apps/web/vercel.json and will fire automatically on production deploy.

crons_scheduled:
  payment-reminders:
    path: /api/cron/payment-reminders
    schedule: "0 9 * * *"              # Daily 9:00 AM UTC
    status: active
    summary: Send renewal reminder emails (7d, 3d, 1d before expiry)
    deduplication: per user + billing period + reminder tier
    dependencies:
      db: [user_subscriptions, system_notifications]
      packages: ["@eko/email", "@eko/db"]
      env: [CRON_SECRET, EMAIL_PROVIDER, RESEND_API_KEY]

  payment-escalation:
    path: /api/cron/payment-escalation
    schedule: "0 9 * * *"              # Daily 9:00 AM UTC
    status: active
    summary: Escalation emails for past-due subscriptions (7d, 14d thresholds)
    deduplication: per user + escalation level
    dependencies:
      db: [user_subscriptions, system_notifications]
      packages: ["@eko/email", "@eko/db"]
      env: [CRON_SECRET, EMAIL_PROVIDER, RESEND_API_KEY]

  monthly-usage-report:
    path: /api/cron/monthly-usage-report
    schedule: "0 9 1 * *"              # 1st of month, 9:00 AM UTC
    status: deprecated                  # v1 tables removed; stub only
    summary: No-op stub retained to prevent scheduler errors
    notes: "getMonthlyUsageStats removed during v1 table cleanup"
    action_needed: Remove from vercel.json

  account-anniversaries:
    path: /api/cron/account-anniversaries
    schedule: "0 9 * * *"              # Daily 9:00 AM UTC
    status: active
    summary: Milestone emails/notifications (1w, 1m, 3m, 6m, 1y account age)
    deduplication: per user + milestone subtype
    dependencies:
      db: [user_subscriptions, system_notifications]
      packages: ["@eko/email", "@eko/db"]
      env: [CRON_SECRET]

  daily-cost-report:
    path: /api/cron/daily-cost-report
    schedule: "0 6 * * *"              # Daily 6:00 AM UTC (midnight CST)
    status: active
    summary: Email admins AI cost breakdown by model/feature with 7-day trend
    dependencies:
      db: [ai_cost_log]
      packages: ["@eko/email", "@eko/db", "@eko/config"]
      env: [CRON_SECRET, ADMIN_EMAIL_ALLOWLIST, ANTHROPIC_DAILY_SPEND_CAP_USD]

1b. Not Scheduled (route exists, not in vercel.json) (8)

These cron routes exist as code but have no vercel.json entry. They must be triggered manually, added to the scheduler, or are handled by external orchestration.

crons_unscheduled:
  ingest-news:
    path: /api/cron/ingest-news
    intended_schedule: "*/15 * * * *"   # Every 15 minutes
    status: active
    summary: Enqueue INGEST_NEWS messages for all configured news providers x active topics
    dependencies:
      db: [topic_categories]
      queue: INGEST_NEWS
      env: [CRON_SECRET, NEWS_API_KEY, GOOGLE_NEWS_API_KEY, THENEWS_API_KEY, NEWSDATA_API_KEY, EVENT_REGISTRY_API_KEY]
    notes: Primary news pipeline trigger; high frequency

  cluster-sweep:
    path: /api/cron/cluster-sweep
    intended_schedule: "0 * * * *"      # Every hour
    status: active
    summary: Find unclustered news sources (>1h old) and enqueue CLUSTER_STORIES batches
    dependencies:
      db: [news_sources]
      queue: CLUSTER_STORIES
      env: [CRON_SECRET]

  generate-evergreen:
    path: /api/cron/generate-evergreen
    intended_schedule: "0 3 * * *"      # Daily 3:00 AM UTC
    status: active
    summary: Enqueue GENERATE_EVERGREEN for active topics up to daily quota
    dependencies:
      db: [topic_categories]
      queue: GENERATE_EVERGREEN
      env: [CRON_SECRET, EVERGREEN_ENABLED, EVERGREEN_DAILY_QUOTA]
    gate: EVERGREEN_ENABLED must be true

  validation-retry:
    path: /api/cron/validation-retry
    intended_schedule: "0 */4 * * *"    # Every 4 hours
    status: active
    summary: Re-enqueue stuck pending validations (not updated in 4+ hours)
    dependencies:
      db: [fact_records]
      queue: VALIDATE_FACT
      env: [CRON_SECRET]

  archive-content:
    path: /api/cron/archive-content
    intended_schedule: "0 2 * * *"      # Daily 2:00 AM UTC
    status: active
    summary: Promote high-engagement facts to enduring; archive expired facts
    dependencies:
      db: [fact_records]
      env: [CRON_SECRET]
    thresholds: 50+ interactions for enduring promotion

  topic-quotas:
    path: /api/cron/topic-quotas
    intended_schedule: "0 6 * * *"      # Daily 6:00 AM UTC
    status: active
    summary: Audit daily fact counts vs topic quotas; log warnings for over/under
    dependencies:
      db: [topic_categories, fact_records]
      env: [CRON_SECRET]
    notes: Monitoring only; does not enqueue work

  import-facts:
    path: /api/cron/import-facts
    intended_schedule: "0 4 * * *"      # Daily 4:00 AM UTC
    status: stub
    summary: Placeholder for structured API imports (ESPN, WikiQuote, GeoNames)
    notes: No-op; awaiting external API integrations

  daily-digest:
    path: /api/cron/daily-digest
    status: deprecated                  # v1 notification_deliveries table removed
    summary: Stub retained to prevent scheduler errors
    action_needed: Delete route and any references

2. Background Workers

Five Bun-based queue consumer processes. Each exposes a /health endpoint and implements graceful shutdown.

workers:
  worker-ingest:
    app: apps/worker-ingest
    status: active
    health_port: 8080                   # configurable via PORT
    heartbeat_interval: 30s
    lease_duration: 2m
    queues:
      - name: INGEST_NEWS
        summary: Fetch articles from news APIs (NewsAPI, GNews, TheNewsAPI, Newsdata.io, Event Registry)
      - name: CLUSTER_STORIES
        summary: Cluster articles into stories via TF-IDF cosine similarity
      - name: RESOLVE_IMAGE
        summary: Resolve and cache images for fact records
      - name: RESOLVE_CHALLENGE_IMAGE
        summary: Resolve anti-spoiler images for challenges where fact image would reveal the answer
    dependencies:
      env: [UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY]
      optional: [SENTRY_DSN]

  worker-facts:
    app: apps/worker-facts
    status: active
    health_port: 8080
    heartbeat_interval: 30s
    lease_duration: 2m
    concurrency: WORKER_CONCURRENCY     # default 1, set 3-5 for seed pipeline
    queues:
      - name: EXTRACT_FACTS
        summary: AI-extract structured facts from news stories
      - name: IMPORT_FACTS
        summary: Bulk import from external APIs (stub)
      - name: GENERATE_EVERGREEN
        summary: AI-generate timeless knowledge facts
      - name: EXPLODE_CATEGORY_ENTRY
        summary: Expand seed entries into structured facts
      - name: FIND_SUPER_FACTS
        summary: Discover cross-entry fact correlations
      - name: GENERATE_CHALLENGE_CONTENT
        summary: Pre-generate challenge content per quiz style
    dependencies:
      env: [UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY]
      ai: [ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY]  # at least one
      optional: [SENTRY_DSN, WORKER_CONCURRENCY]

  worker-validate:
    app: apps/worker-validate
    status: active
    health_port: 8080
    heartbeat_interval: 30s
    lease_duration: 2m
    queues:
      - name: VALIDATE_FACT
        summary: "4-phase validation: structural → consistency → cross-model (gemini-2.5-flash) → evidence (APIs + gemini-2.5-flash)"
    dependencies:
      env: [UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, GOOGLE_API_KEY]
      optional: [SENTRY_DSN]

  worker-reel-render:
    app: apps/worker-reel-render
    status: active
    health_port: 8080
    queues: []                           # Consumes from R2/internal triggers, not Upstash queues
    summary: Render reel videos from fact records using ffmpeg
    dependencies:
      env: [SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY]
      services: [R2, ffmpeg]
      optional: [SENTRY_DSN]

  worker-sms:
    app: apps/worker-sms
    status: active
    health_port: 8080
    queues:
      - name: SEND_SMS
        summary: Send SMS notifications via Twilio
    dependencies:
      env: [UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY]
      services: [Twilio]
      optional: [SENTRY_DSN]

3. Queue System

Backend: Upstash Redis (REST API). Max 3 attempts before dead-letter queue.

queue_infrastructure:
  backend: upstash_redis
  max_attempts: 3
  dlq_suffix: ":dlq"
  soak_isolation: SOAK_QUEUE_SUFFIX    # env var for test isolation
  backoff: exponential_with_jitter     # 250ms to 5m

queue_types:
  # --- Ingestion Pipeline ---
  INGEST_NEWS:
    consumer: worker-ingest
    status: active
    trigger: cron/ingest-news (every 15m)

  CLUSTER_STORIES:
    consumer: worker-ingest
    status: active
    trigger: cron/cluster-sweep (hourly)

  RESOLVE_IMAGE:
    consumer: worker-ingest
    status: active
    trigger: post-extraction (automatic)

  RESOLVE_CHALLENGE_IMAGE:
    consumer: worker-ingest
    status: active
    trigger: post-challenge-generation (automatic)

  # --- Fact Engine ---
  EXTRACT_FACTS:
    consumer: worker-facts
    status: active
    trigger: post-clustering (automatic)

  IMPORT_FACTS:
    consumer: worker-facts
    status: stub
    trigger: cron/import-facts (not active)

  GENERATE_EVERGREEN:
    consumer: worker-facts
    status: active
    trigger: cron/generate-evergreen (daily)
    gate: EVERGREEN_ENABLED=true

  EXPLODE_CATEGORY_ENTRY:
    consumer: worker-facts
    status: active
    trigger: seed pipeline (manual/batch)

  FIND_SUPER_FACTS:
    consumer: worker-facts
    status: active
    trigger: seed pipeline (manual/batch)

  GENERATE_CHALLENGE_CONTENT:
    consumer: worker-facts
    status: active
    trigger: post-extraction or seed pipeline

  # --- Validation ---
  VALIDATE_FACT:
    consumer: worker-validate
    status: active
    trigger: post-extraction (automatic) + cron/validation-retry (every 4h)

  # --- Deprecated ---
  SEND_SMS:
    consumer: worker-sms
    status: active
    notes: SMS delivery via Twilio; legacy tables removed in migration 0118 but queue is active

4. API Surface

4a. Core Content APIs

api_content:
  health:
    method: GET
    path: /api/health
    auth: none
    summary: Uptime check; returns { status, timestamp }

  feed:
    method: GET
    path: /api/feed
    auth: optional
    summary: "Blended fact feed: 40% recent, 30% review-due, 20% evergreen, 10% random"
    params: [limit (max 100), offset, topic]
    notes: Public users get simple chronological feed

  review:
    method: GET
    path: /api/review
    auth: required
    summary: Facts due for spaced repetition (next_review_at passed, streak < 5)
    params: [limit (max 50, default 10)]

  card_detail:
    method: GET
    path: /api/cards/[slug]
    auth: required + subscription
    summary: Full card detail (fact, topic, schema, source story)
    gate: Active subscription or trial required

  entity_detail:
    page: /entity/[id]
    auth: required + subscription
    summary: Entity detail page  header, FCG grid, linked entities
    gate: Active subscription or trial required

  explore:
    page: /explore
    auth: required
    summary: Browse and search entities with infinite scroll

4b. Entity APIs

api_entities:
  search:
    method: GET
    path: /api/entities
    auth: required
    summary: Search and list entities with text search, topic filter, pagination
    params: [q (search text), topic (category UUID), limit (max 50), offset, orderBy]
    gate: Only returns entities with 1+ validated facts

  entity_facts:
    method: GET
    path: /api/entities/[id]/facts
    auth: required
    summary: Fetch facts for a given seed entry

  entity_links:
    method: GET
    path: /api/entities/[id]/links
    auth: required
    summary: Fetch linked entities for graph traversal

4c. Card Interaction APIs

api_interactions:
  interact:
    method: POST
    path: /api/cards/[slug]/interact
    auth: required
    summary: "Record interaction: viewed, expanded, answered, bookmarked, shared"
    features:
      - AI scoring for free_text format (moderateTextAnswer + scoreTextAnswer)
      - Spaced repetition via calculateNextReview()
    rate_limit: 50 free-text answers/user/day

  dispute:
    method: POST
    path: /api/cards/[slug]/dispute
    auth: required
    summary: Submit score dispute for AI re-evaluation
    rate_limit: 10 disputes/user/month
    decisions: [upheld, adjusted, reversed]

4c. Billing APIs

api_billing:
  stripe_webhooks:
    method: POST
    path: /api/stripe/webhooks
    auth: stripe_signature
    summary: Handle Stripe events (subscription changes, payment failures)
    idempotency: billing_webhook_events table (stripe_event_id)
    env: [STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET]

  stripe_portal:
    method: POST
    path: /api/stripe/portal
    auth: required
    summary: Create Stripe Customer Portal session for subscription management
    env: [STRIPE_SECRET_KEY]

  stripe_checkout:
    method: POST
    path: /api/stripe/checkout
    auth: required
    summary: Create Stripe Checkout session for subscription upgrade
    plans: [base, pro, team, plus]
    env: [STRIPE_SECRET_KEY, STRIPE_PRICE_*]

4d. Account & User APIs

api_account:
  notifications:
    methods: [GET, PATCH]
    path: /api/account/notifications
    auth: required
    summary: Get/update notification preferences (email_enabled, digest_frequency, quiet_hours)

  profile:
    methods: [GET, PATCH]
    path: /api/account/profile
    auth: required
    summary: Get/update profile (display_name, timezone, onboarding_completed_at)

  rewards:
    methods: [GET, POST]
    path: /api/user/rewards
    auth: required
    summary: "GET: milestones + cumulative score. POST: claim milestone to extend subscription"

4e. Challenge APIs

api_challenges:
  formats:
    method: GET
    path: /api/challenge-formats
    auth: none (public)
    summary: List all active challenge formats with styles and eligible topic IDs
    cache: 1 hour (public)

  formats_by_topic:
    method: GET
    path: /api/challenge-formats/for-topic/[topicSlug]
    auth: none (public)
    summary: Challenge formats filtered by topic

  format_detail:
    method: GET
    path: /api/challenge-formats/[slug]
    auth: none (public)
    summary: Single challenge format detail

  create_session:
    method: POST
    path: /api/challenge-sessions
    auth: required
    summary: Start a new conversational challenge session
    rate_limit: 5 sessions/user/day

  end_session:
    method: POST
    path: /api/challenge-sessions/[sessionId]/end
    auth: required
    summary: End session early; calculates final score

  turn:
    method: POST
    path: /api/challenge-sessions/[sessionId]/turn
    auth: required
    summary: Process conversational turn (moderation + AI reasoning + scoring)
    max_input: 2000 characters

4f. External Webhooks

api_webhooks:
  twilio:
    method: POST
    path: /api/webhooks/twilio
    status: deprecated
    summary: SMS delivery status stub; always returns 200
    action_needed: Remove route after Twilio config cleanup

  email_test:
    method: POST
    path: /api/email/test
    auth: required
    summary: Send test email to authenticated user
    env: [EMAIL_PROVIDER, RESEND_API_KEY]

5. Admin Dashboard

The admin app (apps/admin) serves admin.eko.day and provides operational screens.

admin_pages:
  # --- Active v2 Screens ---
  dashboard:        { path: "/", summary: "Overview metrics and health" }
  billing:          { path: "/billing", summary: "Revenue overview and plan distribution" }
  billing_customer: { path: "/billing/customers/[id]", summary: "Individual customer billing detail" }
  queue:            { path: "/queue", summary: "Queue depths, DLQ counts, processing rates" }
  users:            { path: "/users", summary: "User list with search and filters" }
  user_detail:      { path: "/users/[id]", summary: "Individual user profile and activity" }
  user_dashboard:   { path: "/users/[id]/dashboard", summary: "User-level metrics" }
  user_notifs:      { path: "/users/[id]/notifications", summary: "User notification history" }
  content:          { path: "/content", summary: "Fact records browser and moderation" }
  feature_flags:    { path: "/feature-flags", summary: "Runtime feature flag toggles" }
  onboarding:       { path: "/onboarding", summary: "Onboarding funnel metrics" }
  audience:         { path: "/audience", summary: "User segmentation and engagement" }

  # --- Legacy v1 Screens (may need update/removal) ---
  urls:             { path: "/urls", summary: "URL tracking list (v1 legacy)" }
  url_detail:       { path: "/urls/[id]", summary: "URL detail and change history (v1 legacy)" }
  changes:          { path: "/changes", summary: "URL change feed (v1 legacy)" }
  change_detail:    { path: "/changes/[id]", summary: "Individual change detail (v1 legacy)" }
  domains_pending:  { path: "/domains/pending", summary: "Domains awaiting approval (v1 legacy)" }
  domains_rejected: { path: "/domains/rejected", summary: "Rejected domains (v1 legacy)" }
  diff_settings:    { path: "/diff-settings", summary: "Diff algorithm config (v1 legacy)" }
  feeder_sources:   { path: "/feeder-sources", summary: "RSS/feed source management (v1 legacy)" }
  media:            { path: "/media", summary: "Media asset browser" }
  avatars:          { path: "/avatars", summary: "User avatar management" }
  user_urls:        { path: "/users/[id]/urls/add", summary: "Add URLs for user (v1 legacy)" }

6. Environment Controls

Central config lives in packages/config/src/index.ts. All env vars are loaded from .env.local at monorepo root.

6a. AI & Cost Controls

ai_controls:
  AI_PROVIDER: google                  # anthropic | openai | google (default: gemini-3-flash-preview)
  ANTHROPIC_DAILY_SPEND_CAP_USD: 5     # Daily budget cap (default $5)
  OPUS_ESCALATION_ENABLED: false       # Allow routing to Opus for complex tasks
  OPUS_MAX_DAILY_CALLS: 20             # Hard cap on Opus invocations
  FACT_EXTRACTION_BATCH_SIZE: 10       # Facts processed per AI call
  VALIDATION_MIN_SOURCES: 2            # Minimum sources for multi-source validation
  NOTABILITY_THRESHOLD: 0.6            # Minimum score to retain a fact (0.0-1.0)

6b. Pipeline Controls

pipeline_controls:
  EVERGREEN_ENABLED: false             # Master switch for evergreen generation
  EVERGREEN_DAILY_QUOTA: 20            # Max evergreen facts per day
  NEWS_INGESTION_INTERVAL_MINUTES: 15  # Cron frequency
  CRON_BATCH_SIZE: 100                 # Max items per cron batch
  WORKER_CONCURRENCY: 1                # Parallel handlers per queue (set 3-5 for seeding)

6c. External Services

services:
  # Database
  SUPABASE_URL: required
  SUPABASE_ANON_KEY: required
  SUPABASE_SERVICE_ROLE_KEY: required

  # Queue
  UPSTASH_REDIS_REST_URL: optional     # Required for workers/crons
  UPSTASH_REDIS_REST_TOKEN: optional

  # Email
  EMAIL_PROVIDER: none                 # none | resend
  RESEND_API_KEY: optional
  RESEND_FROM_EMAIL: optional

  # Error Tracking
  ERROR_TRACKING_PROVIDER: none        # none | sentry
  SENTRY_DSN: optional

  # Billing
  STRIPE_SECRET_KEY: optional          # Required for billing features
  STRIPE_WEBHOOK_SECRET: optional

  # News APIs
  NEWS_API_KEY: optional
  GOOGLE_NEWS_API_KEY: optional
  THENEWS_API_KEY: optional
  NEWSDATA_API_KEY: optional
  EVENT_REGISTRY_API_KEY: optional

  # Image APIs
  UNSPLASH_ACCESS_KEY: optional
  PEXELS_API_KEY: optional

  # Enrichment
  BRANDFETCH_API_KEY: optional
  PDL_API_KEY: optional

7. Operational Debt & Action Items

action_items:
  - id: OPS-001
    priority: low
    action: Remove monthly-usage-report from vercel.json
    reason: Deprecated stub (v1 tables removed)
    file: apps/web/vercel.json

  - id: OPS-002
    priority: low
    action: Remove daily-digest cron route
    reason: Deprecated stub (v1 notification_deliveries removed)
    file: apps/web/app/api/cron/daily-digest/route.ts

  - id: OPS-003
    priority: low
    action: Remove twilio webhook route
    reason: SMS delivery tables dropped; stub only
    file: apps/web/app/api/webhooks/twilio/route.ts

  - id: OPS-004
    priority: medium
    action: Add 8 unscheduled crons to vercel.json before production deploy
    reason: Active pipeline crons (ingest-news, cluster-sweep, etc.) have no scheduler
    file: apps/web/vercel.json

  - id: OPS-005
    priority: high
    action: Deploy to production to activate cron scheduling
    reason: All deployments are preview; crons only fire on production

  - id: OPS-006
    priority: low
    action: Audit SEND_SMS queue usage and worker-sms integration
    reason: Worker exists but legacy SMS tables were dropped; verify end-to-end flow

  - id: OPS-007
    priority: low
    action: Remove legacy v1 admin pages (urls, changes, domains, diff-settings, feeder-sources)
    reason: v1 tables removed; pages likely broken or empty

8. Pipeline Flow Diagram

News APIs ─┐
           ├──▶ [INGEST_NEWS] ──▶ worker-ingest ──▶ news_sources table
           │                                              │
           │                        ┌─────────────────────┘
           │                        ▼
           │              [CLUSTER_STORIES] ──▶ worker-ingest ──▶ story_clusters
           │                                              │
           │                        ┌─────────────────────┘
           │                        ▼
           │              [EXTRACT_FACTS] ──▶ worker-facts ──▶ fact_records
           │                                              │
           │                  ┌───────────┬───────────────┘
           │                  ▼           ▼
           │         [VALIDATE_FACT]  [RESOLVE_IMAGE]
           │              │               │
           │              ▼               ▼
           │         worker-validate  worker-ingest
           │              │               │
           │              ▼               ▼
           │         fact verified    image cached
           │
Seed Data ─┤
           ├──▶ [EXPLODE_CATEGORY_ENTRY] ──▶ worker-facts ──▶ fact_records
           ├──▶ [FIND_SUPER_FACTS] ──▶ worker-facts ──▶ cross-correlations
           └──▶ [GENERATE_CHALLENGE_CONTENT] ──▶ worker-facts ──▶ challenge_content
                                                                        │
                                        ┌───────────────────────────────┘
                                        ▼ (if image would spoil answer)
                              [RESOLVE_CHALLENGE_IMAGE] ──▶ worker-ingest ──▶ anti-spoiler image

Evergreen ────▶ [GENERATE_EVERGREEN] ──▶ worker-facts ──▶ fact_records

9. Recent Ops Logs

All operational events are logged in docs/ops/logs/ with structured frontmatter for tracking.

After any notable operational event (deploy, incident, config change, cron issue), create a log file using the template and update the monthly index.

Event Types at a Glance

TypeWhen to Log
deployProduction deployments, promotion from preview
cron-runManual cron triggers, notable scheduled failures
incidentService degradation, errors, outages
config-changeEnv var changes, feature flag toggles
maintenanceCleanup, migration execution, deprecation removal
worker-issueWorker crashes, stuck queues, DLQ growth
queue-drainManual DLQ reprocessing
billing-eventStripe webhook issues, subscription anomalies
cost-alertAI spend cap hit, budget anomalies

Reference: Quick Commands

# Check worker health
curl http://localhost:8080/health          # Any worker

# Trigger a cron manually (dev)
curl -X POST http://localhost:3000/api/cron/ingest-news \
  -H "Authorization: Bearer $CRON_SECRET"

# Check queue depths (requires Upstash dashboard or Redis CLI)
# Queue names: queue:ingest_news, queue:cluster_stories, etc.
# DLQ names:   queue:ingest_news:dlq, etc.

# Run all workers locally
bun run dev:worker-ingest
bun run dev:worker-facts
bun run dev:worker-validate

# View AI cost report
curl -X POST http://localhost:3000/api/cron/daily-cost-report \
  -H "Authorization: Bearer $CRON_SECRET"