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.
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 |
CachedWordPressService → WordPressPostsService |
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 theAppDepsobject (leads,rateLimit,email,crm,v0,clickup,recaptcha,wordpress,llm,agent).- A global middleware in
index.tsrunsc.set('deps', buildDeps(c.env))on every request; handlers read it withc.get('deps'). - Durable Objects build their own narrowed dep sets:
buildProcessLeadDeps(env)(PrototypeQueue) andbuildConfirmationEmailDeps(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→ orderedstring[];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 anextPollAftertimestamp. - 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 theTothread rather than via BCC. - Alarm-based; retries per
CONFIRMATION_EMAIL_RETRY_CONFIG, then marks the job failed.
Request lifecycle¶
unmatched routes → notFound (RESOURCE_NOT_FOUND)
Global middleware applied to every request (in order):
logger()andprettyJSON()(Hono built-ins).- Request ID — sets an
x-request-idresponse header (from the incoming header or a fresh UUID) for log correlation. - Dependency injection —
c.set('deps', buildDeps(c.env)). - CORS — pattern matching from
CORS_ALLOWED_ORIGINS(+ legacyCLIENT_URL/CLIENT_HOST), with wildcard support viautils/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.