Architecture

The worker follows Clean Architecture (domain-driven design) with four layers and strict dependency rules. All dependencies point inward — outer layers depend on inner layers, never the reverse.

flowchart TB P["Presentation
routes, middleware, validators, templates"] A["Application
use cases, ports, shared helpers"] D["Domain
entities, repository interfaces, errors"] I["Infrastructure
adapters: Pipedrive, Mailgun, V0, KV, …"] P --> A A --> D I --> A I --> D

Layers and dependency rules

Layer Path Responsibility May depend on
Domain src/domain/ Entities, value objects, repository interfaces, error hierarchy, Result type. Zero external dependencies. (nothing)
Application src/application/ Use cases and business logic. Defines ports (interfaces) under application/shared/ports/ for everything external. Domain
Infrastructure src/infrastructure/ Concrete adapters implementing the ports (Mailgun, Pipedrive, V0, KV, LiteLLM, Agno, WordPress, …) plus the DI factory. Application, Domain
Presentation src/presentation/ HTTP handlers, routers, middleware, validators. Wires use cases to deps via context. Application, Domain

Dependency inversion is enforced through ports in application/shared/ports/:

Port Interface Infrastructure adapter
email.port.ts IEmailService MailgunEmailService
crm.port.ts ICrmService PipedriveCrmService
prototype.port.ts IPrototypeService V0PrototypeService
notification.port.ts INotificationService ClickupNotificationService
recaptcha.port.ts IRecaptchaService RecaptchaService
wordpress.port.ts IWordPressService CachedWordPressServiceWordPressPostsService
llm.port.ts ILlmService LiteLlmService
agent.port.ts IAgentService AgnoAgentService
lead.repository.ts (domain) ILeadRepository LeadKvRepository
rate-limit.repository.ts (domain) IRateLimitRepository RateLimitKvRepository
sse.port.ts SSE response helpers (used by streaming handlers)

Dependency injection

A single factory builds every dependency: src/infrastructure/deps.factory.ts.

  • buildDeps(env) returns the AppDeps object (leads, rateLimit, email, crm, v0, clickup, recaptcha, wordpress, llm, agent).
  • A global middleware in index.ts runs c.set('deps', buildDeps(c.env)) on every request; handlers read it with c.get('deps').
  • Durable Objects build their own narrowed dep sets: buildProcessLeadDeps(env) (PrototypeQueue) and buildConfirmationEmailDeps(env) (ConfirmationEmailQueue).

Note

Because every construction lives in one factory, swapping or reconfiguring a service (e.g. toggling Pipedrive with PIPEDRIVE_ENABLED) is a one-line change.

Project structure

src/
├── index.ts                       # Entry point: middleware, route mounting, scheduled handler, DO exports
│
├── domain/
│   ├── lead/                      # lead.entity, lead.repository (interface), prototype.entity, lead-options.constants
│   ├── post/                      # post.types
│   ├── rate-limit/                # rate-limit.entity (bucket logic), rate-limit.repository (interface)
│   └── shared/                    # errors.ts (error hierarchy), result.ts (Result type)
│
├── application/
│   ├── leads/                     # use cases: create-lead, classify-submission, notify-lead-created,
│   │                              #   generate-prototype, stream-prototype, finalize-generation,
│   │                              #   trigger-v0-prototype, poll-deployment, lookup-lead,
│   │                              #   process-lead-background, send-confirmation-email
│   └── shared/
│       ├── ports/                 # interfaces (email, crm, prototype, notification, recaptcha,
│       │                          #   wordpress, llm, agent, sse)
│       ├── chat-history.store.ts  # KV-backed chat transcript store
│       ├── chat-session.store.ts  # KV session→IP mapping for rate-limit attribution
│       ├── sse-progress.ts        # SSE writer + progress estimation
│       ├── v0-prompt.ts           # V0 prompt builders
│       ├── prototype-queue.ts     # job/queue types + retry config
│       ├── confirmation-email-queue.ts # email job types + retry config
│       └── email-utils.ts         # to/bcc combination helpers
│
├── infrastructure/
│   ├── agno/                      # AgnoAgentService (WebSocket streaming + REST)
│   ├── clickup/                   # ClickupNotificationService
│   ├── kv/                        # LeadKvRepository, RateLimitKvRepository
│   ├── litellm/                   # LiteLlmService (classification)
│   ├── mailgun/                   # MailgunEmailService
│   ├── pagespeed/                 # PageSpeed Insights diagnostic service
│   ├── pipedrive/                 # PipedriveCrmService
│   ├── recaptcha/                 # RecaptchaService
│   ├── seo/                       # SEO analyzer service
│   ├── v0/                        # V0PrototypeService (v0-sdk)
│   ├── wordpress/                 # WordPressPostsService + CachedWordPressService
│   └── deps.factory.ts            # DI factory (buildDeps / buildProcessLeadDeps / buildConfirmationEmailDeps)
│
├── presentation/
│   ├── middleware/                # rate-limit, error-handler, admin-auth, cache-auth, chat-auth
│   ├── routes/                    # analytics, chat, contact, diagnostic, leads, posts, seo, support, admin
│   └── validators/                # leads, tickets, url
│
├── durable-objects/
│   ├── prototype-queue.ts         # V0 polling job queue (alarm-based)
│   └── confirmation-email-queue.ts# delayed non-prototype confirmation emails
│
├── scheduled/
│   └── prototype-checker.ts       # hourly safety-net for orphaned prototypes
│
├── templates/                     # HTML+text+i18n email/page templates per service type
├── types/                         # bindings.ts (Cloudflare bindings + AppDeps) and email/data shapes
└── utils/cors.ts                  # CORS wildcard pattern matching

Durable Objects

Both are declared in wrangler.toml and use new_sqlite_classes migrations (free-plan compatible). They are exported from index.ts.

PrototypeQueue (PROTOTYPE_QUEUE)

A serialized, persistent job queue for polling V0 prototype deployment status.

  • HTTP surface (called by the worker, not the public): POST /enqueue, GET /status, POST /clear.
  • Storage schema: job:{leadId}PrototypeJob; queue → ordered string[]; deadLetter → failed jobs; stats → counters.
  • Processes up to MAX_CONCURRENT_JOBS (5) ready jobs per alarm cycle; isolates each job's failure.
  • Self-reschedules via alarms. A job that is still generating in V0 is re-checked after POLL_RETRY_DELAY_MS (2 min) via a nextPollAfter timestamp.
  • Transient errors retry with ERROR_RETRY_CONFIG; exhausted jobs move to the dead-letter queue.

ConfirmationEmailQueue (CONFIRMATION_EMAIL_QUEUE)

A delayed-send queue for non-prototype confirmation emails.

  • HTTP surface: POST /enqueue, GET /status.
  • Storage schema: job:{leadId}:{emailType}ConfirmationEmailJob; stats.
  • Delays sending by CONFIRMATION_EMAIL_DELAY_MINUTES (default 2) so the sales manager appears in the To thread rather than via BCC.
  • Alarm-based; retries per CONFIRMATION_EMAIL_RETRY_CONFIG, then marks the job failed.

Request lifecycle

sequenceDiagram participant Client participant Hono as Hono app (index.ts) participant MW as Middleware chain participant Route as Route handler participant UC as Use case participant Infra as Infrastructure adapter Client->>Hono: HTTP request Hono->>MW: logger → prettyJSON → request-id → buildDeps → CORS MW->>Route: matched route (+ route-specific middleware, e.g. rate-limit/auth) Route->>Route: validate input Route->>UC: invoke use case with deps from c.get('deps') UC->>Infra: call ports (KV, Pipedrive, V0, …) Infra-->>UC: result UC-->>Route: Result / data Route-->>Client: c.json(...) or SSE stream Note over Hono: thrown errors → globalErrorHandler → structured JSON
unmatched routes → notFound (RESOURCE_NOT_FOUND)

Global middleware applied to every request (in order):

  1. logger() and prettyJSON() (Hono built-ins).
  2. Request ID — sets an x-request-id response header (from the incoming header or a fresh UUID) for log correlation.
  3. Dependency injectionc.set('deps', buildDeps(c.env)).
  4. CORS — pattern matching from CORS_ALLOWED_ORIGINS (+ legacy CLIENT_URL / CLIENT_HOST), with wildcard support via utils/cors.ts.

Background work uses ctx.waitUntil(...) so side effects (emails, notifications, enqueues) outlive the response without blocking it. The scheduled export handles the cron trigger.