# Architecture (/docs/developer-guide/architecture)

## Monorepo Structure [#monorepo-structure]

Beai 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 [#tech-stack]

| Layer | Technology |
|-------|-----------|
| **Language** | TypeScript 5.x |
| **Framework** | TanStack Start (App Router, SSR) |
| **Styling** | Tailwind CSS v4 |
| **Components** | shadcn/ui (new-york style) |
| **Data Fetching** | SWR |
| **Database** | PostgreSQL with Drizzle ORM |
| **Job Queue** | pg-boss (Postgres-based) |
| **Testing** | Vitest |
| **Linting** | Biome |
| **Monorepo** | Turborepo + pnpm workspaces |

## How the Apps Connect [#how-the-apps-connect]

For the runtime topology (web, worker, Postgres, LLM providers), see the
[System Architecture diagram](/docs/getting-started#system-architecture).

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 [#deployment-modes]

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

| Mode | Description |
|------|-------------|
| `local` | Single-org, email/password auth |
| `demo` | Read-only demonstration |
| `whitelabel` | Multi-org with Auth0 SSO |
| `cloud` | Managed multi-tenant, self-serve signup with Stripe billing |

## Database [#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 [#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](/docs/user-guide/providers).
