Development

Local development

Prerequisites: Node.js (LTS), npm, and a Cloudflare account with Workers enabled (Wrangler is a project dependency).

npm install
cp .dev.vars.example .dev.vars   # fill in local secrets (see env vars below)
npm run dev                      # wrangler dev --ip 0.0.0.0

.dev.vars holds local secret values (gitignored). Non-sensitive defaults live in wrangler.toml [vars].

Scripts

Script Command Description
npm run dev wrangler dev --ip 0.0.0.0 Local dev server
npm run build tsc && node scripts/build.js Type-check, then bundle with esbuild
npm run deploy wrangler deploy Deploy to Cloudflare Workers (default/prod env)
npm run lint eslint . Lint
npm test vitest Run the test suite

Build system

  • tsc type-checks the project (path alias @/*src/*).
  • scripts/build.js bundles src/index.ts with esbuild to dist/index.js: ESM, target es2022, platform: neutral, minified, no sourcemap, process.env.NODE_ENV defined to production.
  • wrangler.toml points main at src/index.ts; Wrangler handles bundling for dev/deploy.
  • A helper script scripts/backfill-lead-email-index.js backfills the leadByEmail: KV index for leads created before the index existed.

Testing

Tests use Vitest and cover all layers under tests/ (application/, domain/, infrastructure/, presentation/, with shared helpers/fixtures.ts and helpers/mocks.ts).

npm test                                  # all tests
npx vitest --watch                        # watch mode
npx vitest tests/application/             # one layer
npx vitest tests/domain/lead/lead.entity.test.ts  # one file

Configuration / environment variables

Set non-secret defaults in wrangler.toml and secrets via Wrangler. Never commit secret values.

Bindings (wrangler.toml)

Binding Type Notes
CACHE KV namespace Primary storage for leads, rate limits, contacts, analytics, tickets, posts cache, chat history
PROTOTYPE_QUEUE Durable Object Class PrototypeQueue (SQLite migration v1)
CONFIRMATION_EMAIL_QUEUE Durable Object Class ConfirmationEmailQueue (SQLite migration v2)

Variables (names only)

Variable Purpose
NODE_ENV production / development; gates reCAPTCHA skip, dev IP fallbacks, [TEST] email prefixes
CLIENT_URL, CLIENT_HOST Legacy CORS origins
CORS_ALLOWED_ORIGINS Comma-separated allowed origins (supports wildcards; * = all)
API_BASE_URL Base URL used to build preview URLs (defaults to the prod worker URL)
WORDPRESS_POSTS_API WordPress REST API base
PIPEDRIVE_DOMAIN, PIPEDRIVE_API_TOKEN, PIPEDRIVE_BCC, PIPEDRIVE_ENABLED Pipedrive CRM
MAILGUN_DOMAIN, MAILGUN_API_KEY Mailgun
SUPPORT_EMAIL_FROM, SUPPORT_EMAIL_TO, RESULTS_EMAIL_TO, ADMIN_EMAIL, FROM_EMAIL, SALES_MANAGER_EMAIL Email addressing
V0_API_KEY V0 SDK
CLICKUP_API_TOKEN, CLICKUP_TEAM_ID, SALES_CHANNEL_ID ClickUp notifications; prototype lifecycle uses the sales channel
LEAD_ALERT_CHANNEL_ID Direct system-to-Andrés channel for immediate new-lead alerts
LITELLM_API_URL, LITELLM_API_KEY Lead classification
AGNO_API_URL, AGNO_API_KEY Sales-chat agent
SECRET_RECAPTCHA_KEY reCAPTCHA secret
PAGESPEED_API_KEY PageSpeed Insights
RATE_LIMIT_LANDING_MAX, RATE_LIMIT_APPLICATION_MAX Monthly prototype quotas per bucket (default 2)
CONFIRMATION_EMAIL_DELAY_MINUTES Delay before non-prototype confirmation emails (default 2)
SPAM_FILTER_ENABLED, VENDOR_FILTER_ENABLED Gate filtering actions ("false" = classify but don't filter)
CHAT_API_SECRET Trusted-chat / reCAPTCHA-bypass / lookup key (X-Chat-Api-Key) — ships to browser, not truly secret
ADMIN_API_SECRET Admin data API key (X-Admin-Api-Key) — server-only, dedicated
CACHE_API_SECRET Posts cache-purge key (X-Cache-Api-Key)
USE_DO_QUEUE Optional toggle present on bindings

Secrets

npx wrangler secret put PIPEDRIVE_API_TOKEN
npx wrangler secret put MAILGUN_API_KEY
npx wrangler secret put V0_API_KEY
npx wrangler secret put SECRET_RECAPTCHA_KEY
npx wrangler secret put CLICKUP_API_TOKEN
npx wrangler secret put LITELLM_API_KEY
npx wrangler secret put AGNO_API_KEY
npx wrangler secret put CHAT_API_SECRET
npx wrangler secret put ADMIN_API_SECRET
npx wrangler secret put CACHE_API_SECRET

CORS

Pattern matching is implemented in utils/cors.ts and configured via CORS_ALLOWED_ORIGINS (plus legacy CLIENT_URL / CLIENT_HOST). Supports exact domains, wildcard subdomains (https://*.swapps.workers.dev), localhost (http://localhost:*), and * (allow all). Allowed headers include X-Chat-Api-Key.

Rate limiting

Prototype generation is rate-limited per IP and per email — a dual bucket model (rate-limit.middleware.ts, domain/rate-limit/). A request is allowed only when both buckets have remaining quota, defeating the two obvious bypasses (swap email on same IP; swap IP/VPN with same email).

  • Tracked per identifier per calendar month; landing and application counted independently.
  • Counters increment only on successful completion; failures clear the in-progress hold without consuming quota.
  • One generation at a time per identifier (in-progress gate via findInProgressEntry).
  • Storage: KV key rate_limit:{id}:{month}, 90-day TTL.
  • Trusted IP source: CF-Connecting-IP in prod; for trusted chat, Agno-forwarded X-User-IP (only when X-Chat-Api-Key matches). Client-controlled X-Forwarded-For / X-Real-IP are trusted only in non-prod dev.

Exceeding a limit or hitting an in-progress generation returns 429 with details (limit, current, remaining, resetDate, or inProgressLeadId / inProgressType / startedAt).

Cache management

Cache Key prefix TTL
WordPress posts posts_cache: 7 days (safety net; publish webhook purges are the real freshness mechanism)
Diagnostic (PageSpeed) per URL+strategy 1 hour (DIAGNOSTIC_CACHE_TTL = 3600)
SEO analysis per URL 1 hour (SEO_CACHE_TTL = 3600)
Rate limit rate_limit:{ip|email}:{month} 90 days
Lead data lead:{id} (with indexed metadata) persisted; metadata enables fast scans

Posts cache can be bypassed per request with ?cache=false, and purged via the authenticated endpoints (DELETE /api/posts/cache, POST /api/posts/cache/clear), which paginate through every posts_cache: key.

Scheduled jobs

Schedule Handler Purpose
0 * * * * (hourly) scheduled/prototype-checker.ts Safety net: scan KV metadata for orphaned/stuck/needs-email leads and re-enqueue them to the PrototypeQueue DO (which dedups)

The cron handler is a lightweight dispatcher (target < 50 ms CPU): it filters leads using KV metadata (no full reads) with a legacy fallback for pre-metadata keys, then posts the discovered ids to the DO's /enqueue.

Error handling

A domain error hierarchy (domain/shared/errors.ts) maps to HTTP status + error codes; the global handler (presentation/middleware/error-handler.ts) converts thrown errors into a structured response, and unmatched routes return RESOURCE_NOT_FOUND.

Error code HTTP Class
VALIDATION_ERROR 400 ValidationError
AUTHENTICATION_REQUIRED 401 AuthenticationError
AUTHORIZATION_DENIED 403 AuthorizationError
RESOURCE_NOT_FOUND 404 NotFoundError
RESOURCE_CONFLICT 409 ConflictError
DOMAIN_ERROR 422 DomainError
TOO_MANY_REQUESTS 429 TooManyRequestsError
INTERNAL_ERROR / app failure 500 ApplicationError / InfrastructureError
EXTERNAL_SERVICE_ERROR 502 ExternalServiceError

Response shape:

{
  "error": { "code": "VALIDATION_ERROR", "message": "Invalid email format", "requestId": "..." }
}

Every response carries an x-request-id header for log correlation.

Security

  • reCAPTCHA on public forms (/api/leads, /api/support/send, /api/support/tickets); skipped in dev and for trusted chat on /api/leads.
  • API keys via dedicated headers: X-Chat-Api-Key (chat, lookup, reCAPTCHA bypass), X-Admin-Api-Key (admin data), X-Cache-Api-Key (cache purge). ADMIN_API_SECRET is server-only and must not reuse CHAT_API_SECRET.
  • Input validation on POST endpoints; URL validation with SSRF protection on diagnostic/SEO; file-upload limits on tickets (≤5 files).
  • Trusted IP handling — only CF-Connecting-IP (and gated X-User-IP for trusted chat) are trusted in production; spoofable headers are dev-only.
  • LLM spam/vendor filtering (toggleable) keeps junk out of Pipedrive and email.

Deployment

npm run deploy                 # production (default env)
npx wrangler deploy --env preview   # preview / staging
npx wrangler deploy --env dev       # dev
  • wrangler.toml defines three environments: default (production), preview (PR/staging), and dev. Each has its own [vars]; preview has its own KV/DO bindings.
  • GitHub Actions / Cloudflare: pushing a branch triggers an automatic Cloudflare deployment and a preview URL; the convention is to post that URL in the related ClickUp task. After approval, merge to master.
  • Observability logs are enabled ([observability.logs] enabled = true).

Note

Branch and commit conventions: branch CU-<taskId>; commit CU-<taskId> - <type>: <description> (single line).