Architecture

How the Elmo monorepo is structured and how the pieces fit together.

Monorepo Structure

Elmo is a pnpm monorepo managed by Turborepo. The codebase is split into apps (deployable) and packages (shared libraries).

.
├── apps/
│   ├── web/          # Main dashboard (TanStack Start)
│   ├── worker/       # Background job runner (pg-boss)
│   ├── cli/          # @elmohq/cli npm package
│   └── www/          # Marketing site (this site)

├── packages/
│   ├── ui/           # Shared UI components (shadcn/ui)
│   ├── lib/          # Shared business logic, DB schema, AI providers
│   ├── config/       # Deployment configuration types
│   ├── deployment/   # Deployment mode resolution
│   ├── demo/         # Demo mode implementation
│   ├── local/        # Local development mode
│   ├── whitelabel/   # White-label mode
│   ├── og/           # Shared OG image generation
│   └── api-spec/     # OpenAPI specification

├── docker/           # Dockerfile (multi-stage)
├── e2e/              # End-to-end tests
├── biome.json        # Linting and formatting
├── turbo.json        # Task pipeline configuration
└── pnpm-workspace.yaml

Tech Stack

LayerTechnology
LanguageTypeScript 5.x
FrameworkTanStack Start (App Router, SSR)
StylingTailwind CSS v4
Componentsshadcn/ui (new-york style)
Data FetchingSWR
DatabasePostgreSQL with Drizzle ORM
Job Queuepg-boss (Postgres-based)
TestingVitest
LintingBiome
MonorepoTurborepo + pnpm workspaces

How the Apps Connect

For the runtime topology (web, worker, Postgres, LLM providers), see the System Architecture diagram.

What matters for development:

  • The Web App serves the dashboard UI and the REST API (/api/v1).
  • The Worker processes background jobs — querying AI models, running evaluations.
  • Both import shared code from packages/lib (database schema, AI provider config) and packages/config (deployment types).
  • The CLI generates Docker Compose configurations and manages the deployment lifecycle.

Deployment Modes

Each deployment mode is implemented as a separate package in packages/:

PackageModeDescription
packages/locallocalSingle-org, email/password auth
packages/demodemoRead-only demonstration
packages/whitelabelwhitelabelMulti-org with Auth0 SSO

The packages/deployment package resolves the active mode at runtime based on DEPLOYMENT_MODE and provides the correct implementation.

Database

The database schema is defined with Drizzle ORM in packages/lib/src/db/. Key tables include:

  • brands — Brand profiles with name, website, domains, aliases
  • prompts — Tracking prompts associated with brands
  • evaluations — Results from querying AI models
  • citations — URLs cited by AI models in their responses

Migrations are generated with drizzle-kit generate and applied with drizzle-kit migrate.

Provider Abstraction

All AI scraping flows through a single Provider interface in packages/lib/src/providers/. The runtime wiring:

  1. SCRAPE_TARGETS (env var) holds comma-separated model:provider[:version][:online] entries.
  2. parseScrapeTargets() turns the string into ModelConfig[].
  3. getProvider(providerId).run(model, prompt, { webSearch, version }) dispatches to a concrete registry/ implementation and returns a normalized ScrapeResult (textContent, rawOutput, webQueries, citations, modelVersion).

User-facing setup is documented in Providers.

Was this page helpful?