elmo
  • Pricing
  • Changelog
  • Roadmap
  • Vision
  • Docs
505Sign up
IntroductionQuick Start
OverviewDevelopment SetupCommands ReferenceArchitectureConfigurationCloud DeploymentTelemetryAI Contribution PolicyVersioning & Releases
Edit this page·View as Markdown·Report an issue·Join our Discord

Get product updates

New features and releases from Elmo, straight to your inbox.

Product

  • Features
  • Pricing
  • Off-Site AEO
  • Changelog
  • Roadmap

Resources

  • Blog
  • Documentation
  • Support
  • API Reference
  • Elmo Cloud Status
  • Provider Status
  • Issues

Learn

  • What is AEO?
  • What is GEO?
  • AEO Glossary
  • AI Search Guides
  • AI Search Statistics
  • AEO by Industry

Tools

  • Tool Directory
  • Free & Open-Source Tools
  • Compare Tools

Company

  • About
  • Vision
  • Brand Assets
  • Merch
  • llms.txt

© 2026 Blue Whale Software, LLC

TermsPrivacyCookiesSubprocessorsAcceptable Use
v0.7.6Open source on GitHub
Twitter / XLinkedInDiscordGitHub
elmo

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. An answer goes through separate stages, each retrying on its own:
    1. Query (process-prompt): calls the AI model and stores its answer, nothing more. Only this stage calls a paid provider, and nothing after it can make it call again.
    2. Derive (derive-runs): extracts each answer's text and citations, and asks Jev (TypeSafe) which companies it mentions and their sentiment; positions come from where each is first named. A failure retries this stage alone and never falls back to name matching. A sweep every few minutes asks again for answers still unjudged.
    3. Roll up (refresh-rollups): rebuilds the aggregates the dashboard reads. An answer counts once it's judged.
    A config change or a new judge replays history (reprocess) by queueing the brand's stale answers for the derive stage, most recent first, so any number of workers share a backfill.
  • 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

packages/deployment resolves the active mode at runtime from DEPLOYMENT_MODE and supplies that mode's feature flags, branding, and auth hooks:

ModeDescription
localSingle-org, email/password auth
demoRead-only demonstration
whitelabelMulti-org with Auth0 SSO
cloudManaged multi-tenant, self-serve signup with Stripe billing

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.

Never use drizzle-kit push against a deployment. It diffs the live database against the schema files rather than replaying migrations, so it drops any index a migration created that the schema cannot express — INCLUDE indexes, and anything on the generated better-auth tables. It also records nothing in drizzle.__drizzle_migrations, leaving migrate unable to tell what has run.

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?

On this page

Monorepo StructureTech StackHow the Apps ConnectDeployment ModesDatabaseProvider Abstraction