Skip to main content

Deployment Guide

Praeto Dispatcher currently deploys as a Cloudflare Worker with a Cloudflare Queue binding and Neon Postgres as the database.

Requirements

  • Node.js installed
  • npm installed
  • Cloudflare Wrangler installed through npx wrangler
  • Cloudflare account with Workers enabled
  • Cloudflare Queue created
  • Neon Postgres database

Install dependencies

npm install
Required package:
npm install @neondatabase/serverless

Wrangler config

This repo should use wrangler.jsonc as the primary Worker config. Avoid maintaining both wrangler.toml and wrangler.jsonc long-term. If both exist, Wrangler may use the JSONC file depending on the repo state, which can cause confusion. Example wrangler.jsonc shape:
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "dispatcher-api-dev",
  "main": "src/index.ts",
  "compatibility_date": "2026-04-03",
  "compatibility_flags": ["nodejs_compat"],
  "observability": {
    "enabled": true
  },
  "upload_source_maps": true,
  "vars": {
    "ENVIRONMENT": "development",
    "SECRET_ROTATION_OVERLAP_DAYS": "7",
    "RETENTION_EVENT_DAYS": "30",
    "RETENTION_DELIVERY_DAYS": "30",
    "RETENTION_ATTEMPT_DAYS": "30",
    "RETENTION_DLQ_DAYS": "30",
    "USAGE_MONTHLY_EVENT_LIMIT": "10000",
    "USAGE_MONTHLY_ATTEMPT_LIMIT": "50000",
    "USAGE_MAX_ENDPOINTS": "1000",
    "USAGE_MAX_PAYLOAD_BYTES": "262144",
    "USAGE_MAX_FANOUT_ENDPOINTS": "100",
    "RATE_LIMIT_EVENTS_PER_MINUTE": "60",
    "RATE_LIMIT_ENDPOINT_CREATES_PER_HOUR": "30",
    "RATE_LIMIT_ADMIN_REQUESTS_PER_MINUTE": "120"
  },
  "queues": {
    "producers": [
      {
        "binding": "DISPATCH_QUEUE",
        "queue": "dispatcher-events-dev"
      }
    ],
    "consumers": [
      {
        "queue": "dispatcher-events-dev",
        "max_batch_size": 10,
        "max_batch_timeout": 5,
        "max_retries": 5
      }
    ]
  },
  "triggers": {
    "crons": ["0 * * * *"]
  }
}

Secrets

Set database URL:
npx wrangler secret put DATABASE_URL
Set admin key:
npx wrangler secret put ADMIN_API_KEY
Then deploy:
npx wrangler deploy

Database migrations

Apply migrations in order. Important schema areas:
  • tenant_profile
  • api_keys
  • endpoint_destinations
  • events
  • deliveries
  • delivery_attempts
  • rate_limit_counters
Phase 8 requires:
CREATE TABLE IF NOT EXISTS rate_limit_counters (
  counter_key text PRIMARY KEY,
  tenant_id text NOT NULL,
  scope text NOT NULL,
  window_start timestamp without time zone NOT NULL,
  window_seconds integer NOT NULL,
  count integer NOT NULL DEFAULT 0,
  created_at timestamp without time zone DEFAULT now(),
  updated_at timestamp without time zone DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_tenant_scope
  ON rate_limit_counters (tenant_id, scope);

CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_window_start
  ON rate_limit_counters (window_start);

CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_updated_at
  ON rate_limit_counters (updated_at);

Deploy

npx wrangler deploy
Expected output should show bindings such as:
env.DISPATCH_QUEUE
env.ENVIRONMENT
env.SECRET_ROTATION_OVERLAP_DAYS
If expected vars are missing, Wrangler is probably using a different config file than you think.

Tail logs

npx wrangler tail
Use this during tests and delivery debugging.

Health check

Invoke-RestMethod "https://api.praeto.dev/health"

Post-deploy smoke tests

.\tests\Web-Request-GetSafetyLimits.ps1
.\tests\Web-Request-GetUsageLimits.ps1
.\tests\Web-Request-TestIdempotency.ps1
.\tests\Web-Request-TestSSRFProtection.ps1
.\tests\Web-Request-GetMetricsSummary.ps1

Production hardening checklist

Before a production deploy:
  • Use separate production Neon database or branch
  • Use separate production Cloudflare Queue
  • Set production Worker name
  • Rotate ADMIN_API_KEY
  • Confirm all retention values
  • Confirm all usage limits
  • Confirm all rate limits
  • Confirm logs do not leak webhook secrets
  • Confirm endpoint signing secrets are never returned except create/rotate
  • Confirm DLQ replay works
  • Confirm retention dry-run works
  • Confirm rate-limit SQL exists