D Diagent docs

Architecture

Stack & layout

Pitchbar is a single Laravel codebase with two frontends. Backend is Laravel 13 + Octane on PHP 8.3+; admin UI is Inertia v3 + React 19; visitor widget is Preact โ‰ค50KB. AI stack defaults to Cloudflare (Workers AI + Vectorize + Browser Rendering) with OpenAI + Qdrant as fallback.

The stack

LayerTech
App frameworkLaravel 13 (PHP 8.3+)
ServerLaravel Octane on FrankenPHP
RealtimeLaravel Reverb (WebSocket)
QueueLaravel Horizon on Redis
AuthLaravel Fortify (sessions, 2FA, password reset) + Sanctum (API tokens)
BillingLaravel Cashier (Stripe)
DatabasePostgres 16
Cache / sessionsRedis 7
Admin frontendInertia v3 + React 19, Tailwind v4, shadcn/ui (Radix), Vite, strict TS
Typed routesWayfinder (TS bindings to Laravel routes)
Visitor widgetPreact 10 (aliased as React) + Vite + selective Tailwind v4
LLM (preferred)Cloudflare Workers AI โ€” Llama 3.3 70B + bge-base-en-v1.5
LLM (fallback)OpenAI gpt-4o-mini + text-embedding-3-small (and OpenRouter as a router)
Vector store (preferred)Cloudflare Vectorize
Vector store (fallback)Qdrant (HTTP client)
Crawler (preferred)Cloudflare Browser Rendering
Crawler (fallback)Browserless โ†’ plain HTTP
Object storageCloudflare R2 (S3-compatible)
HostingLaravel Cloud
ObservabilitySentry + OpenTelemetry โ†’ Honeycomb / Grafana Cloud

Repository layout

One Laravel app at the repo root. The admin frontend ships as Inertia pages inside the same app; the visitor widget is a second isolated Vite build.

pitchbar/                     โ€” Laravel app
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ Actions/Fortify/      โ€” Fortify hooks (CreateNewUser, etc.)
โ”‚   โ”œโ”€โ”€ Concerns/             โ€” BelongsToWorkspace, BelongsToAgent traits
โ”‚   โ”œโ”€โ”€ Http/
โ”‚   โ”‚   โ”œโ”€โ”€ Controllers/Admin/    โ€” customer + admin Inertia controllers
โ”‚   โ”‚   โ”œโ”€โ”€ Controllers/Widget/   โ€” /api/v1/widget/* (visitor-side, JWT)
โ”‚   โ”‚   โ””โ”€โ”€ Middleware/
โ”‚   โ”œโ”€โ”€ Models/               โ€” Workspace, Agent, Conversation, Plan, โ€ฆ
โ”‚   โ”œโ”€โ”€ Services/
โ”‚   โ”‚   โ”œโ”€โ”€ Rag/              โ€” Retriever, Chunker, PromptBuilder, CuratedAnswerMatcher
โ”‚   โ”‚   โ”œโ”€โ”€ Llm/              โ€” OpenAiHttpClient, WorkersAiClient, Fakes
โ”‚   โ”‚   โ”œโ”€โ”€ Vector/           โ€” VectorizeClient, QdrantHttpClient
โ”‚   โ”‚   โ”œโ”€โ”€ Crawl/            โ€” CloudflareBrowserClient, AutoIndexPageVisit, PlainHttpCrawler
โ”‚   โ”‚   โ”œโ”€โ”€ Triggers/         โ€” CtaSelector, LeadIntentDetector
โ”‚   โ”‚   โ”œโ”€โ”€ Analytics/        โ€” EventStore (analytics rollups + gap detection ride in app/Jobs/Analytics)
โ”‚   โ”‚   โ”œโ”€โ”€ Billing/          โ€” StripeProductSync, MeteredBilling, PayPalClient, RazorpayClient
โ”‚   โ”‚   โ”œโ”€โ”€ Tools/            โ€” ToolRegistry + EscalateToHumanTool (Phase 2)
โ”‚   โ”‚   โ”œโ”€โ”€ Vertical/         โ€” VerticalPresetRegistry + 7 preset classes
โ”‚   โ”‚   โ”œโ”€โ”€ I18n/             โ€” LocaleResolver, LocaleCatalog (132 languages)
โ”‚   โ”‚   โ””โ”€โ”€ Widget/           โ€” WidgetJwt, WidgetCopy, InlineBlockParser
โ”‚   โ”œโ”€โ”€ Jobs/Crawl/           โ€” CrawlSourceJob, CrawlPageJob, IndexDocumentJob
โ”‚   โ”œโ”€โ”€ Jobs/Analytics/       โ€” DetectGapJob (post-stream gap detection)
โ”‚   โ””โ”€โ”€ Events/               โ€” TokenStreamed, TurnCompleted, TurnFailed
โ”œโ”€โ”€ resources/
โ”‚   โ”œโ”€โ”€ js/                   โ€” admin Inertia (default Vite build)
โ”‚   โ”‚   โ”œโ”€โ”€ pages/
โ”‚   โ”‚   โ”œโ”€โ”€ components/
โ”‚   โ”‚   โ””โ”€โ”€ โ€ฆ
โ”‚   โ”œโ”€โ”€ widget/               โ€” visitor widget (separate Vite build)
โ”‚   โ”œโ”€โ”€ views/                โ€” Blade (Inertia root + marketing + docs)
โ”‚   โ””โ”€โ”€ css/app.css           โ€” Tailwind v4 entry
โ”œโ”€โ”€ routes/
โ”‚   โ”œโ”€โ”€ web.php
โ”‚   โ”œโ”€โ”€ api.php
โ”‚   โ””โ”€โ”€ channels.php
โ”œโ”€โ”€ database/{migrations,factories,seeders}
โ”œโ”€โ”€ tests/{Feature,Unit,Browser}
โ”œโ”€โ”€ docs/PLAN.md              โ€” full engineering plan
โ””โ”€โ”€ public/widget/            โ€” built widget bundle

Two frontends, one backend

The admin and customer surfaces are the same Inertia app โ€” same Vite build, same component library. The roles are separated by route group and middleware, not by codebase. This keeps a single source of truth for design tokens, routing helpers, and authentication state.

The visitor widget is the opposite โ€” it intentionally shares nothing with the admin code. It can't import from resources/js/; it has its own Vite config; it has its own router (just a Preact component tree) and its own state. The size budget is a hard 50KB gzipped โ€” admin features cannot bleed in.

Reverb & WebSocket

Reverb runs as a separate process and powers:

  • Inbox live updates โ€” operators see new messages as they arrive.
  • Human takeover events โ€” the visitor's widget gets a "human is here" event when an operator claims the conversation.
  • Operator presence โ€” Available / Away states sync across team members.

Channels are private by default โ€” the widget joins conversation.{id} using its JWT, and the operator app joins workspace.{id} using its session.

Cloudflare one-bill mode

Set CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN and the LLM, vector store, and crawler all auto-bind to Cloudflare. Total external infra cost: $5/month Workers Paid + per-request usage. Replace any one piece (e.g. swap Vectorize for Qdrant by setting QDRANT_URL) and the binding shifts.

Multi-tenant isolation

Every tenant-scoped query is filtered by the BelongsToWorkspace trait's global scope. Crossing the boundary requires an explicit withoutWorkspaceScope() with a justifying comment. There's a regression test that fails the build if a model with a workspace_id column doesn't use the trait. See Multi-tenancy.