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¶
tsctype-checks the project (path alias@/*→src/*).scripts/build.jsbundlessrc/index.tswith esbuild todist/index.js: ESM, targetes2022,platform: neutral, minified, no sourcemap,process.env.NODE_ENVdefined toproduction.wrangler.tomlpointsmainatsrc/index.ts; Wrangler handles bundling fordev/deploy.- A helper script
scripts/backfill-lead-email-index.jsbackfills theleadByEmail: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-IPin prod; for trusted chat, Agno-forwardedX-User-IP(only whenX-Chat-Api-Keymatches). Client-controlledX-Forwarded-For/X-Real-IPare 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_SECRETis server-only and must not reuseCHAT_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 gatedX-User-IPfor 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.tomldefines three environments: default (production),preview(PR/staging), anddev. Each has its own[vars];previewhas 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).