Commands Reference

All development scripts and commands for working on Beai.

Development

# Run all dev servers (turbo)
pnpm dev

# Run only the web app
pnpm --filter @workspace/web dev

# Run only the worker
pnpm --filter @workspace/worker dev

Building

# Build all packages
pnpm build

# Build only the web app
pnpm --filter @workspace/web build

# Type check every package
pnpm turbo check-types

Testing

# Run the unit tests across every package
pnpm test

# Run the tests for scripts/, which sits outside the workspace
pnpm test:scripts

Storybook stories double as browser tests. They run in Vitest's browser mode, so they need a browser installed before the first run:

pnpm --filter @workspace/web exec playwright install chromium
pnpm --filter @workspace/web test:storybook

The analytics stats are checked against a real Postgres. These tests migrate the database in TEST_DATABASE_URL and write to it, so they refuse to run unless it points at a local database whose name ends in _test:

createdb elmo_test
TEST_DATABASE_URL=postgres://postgres:postgres@localhost:5432/elmo_test \
  pnpm --filter @workspace/web test:integration

End-to-End Tests

.github/workflows/e2e.yaml is the source of truth for E2E orchestration — it stands up one Dockerized container per deployment mode, seeds them, and runs the suites in order. Replay the whole thing locally with act:

pnpm test:e2e-local

The scripts below each run one mode's suite against a stack that workflow has already brought up, which is what you want when iterating on a single spec:

# Playwright specs
pnpm test:e2e
pnpm test:e2e:cloud
pnpm test:e2e:whitelabel
pnpm test:e2e:demo

# Bruno API suites
pnpm test:api
pnpm test:api:cloud
pnpm test:api:demo

Code Quality

# Check formatting, lint and import order — the same check CI runs
pnpm lint

# Fix everything Biome can fix on its own
pnpm lint:fix

# Format code with Biome
pnpm format

# Find unused exports and dependencies
pnpm knip

# Lint the hand-written OpenAPI document
pnpm lint:spec

# Check every dependency's license against the MIT-compatible allow-list
pnpm license-check

Always run pnpm format before committing to ensure consistent code style across the codebase.

Database

drizzle.config.ts and the migrations live in packages/lib, so Drizzle runs against that package:

# Generate a new migration after schema changes
pnpm --filter @workspace/lib exec drizzle-kit generate

# Run pending migrations
pnpm --filter @workspace/lib exec drizzle-kit migrate

Apply schema changes with drizzle-kit migrate only. See Architecture for why drizzle-kit push is unsafe against a deployment.

Component Stories

# Start the Storybook dev server on port 6006
pnpm --filter @workspace/web storybook

# Build the static Storybook
pnpm --filter @workspace/web storybook:build

CLI Development

# Run CLI directly from source
pnpm elmo <command>

# Build and link CLI globally for testing
pnpm cli:link

# Unlink global CLI
pnpm cli:unlink

Docker

# Build Docker images locally
elmo compose build

# Build without cache
elmo compose build --no-cache

# Build only web or worker
elmo compose build web
elmo compose build worker

# Run other docker compose commands
elmo compose up -d
elmo compose ps
elmo compose logs -f web
elmo compose down

Versioning

# Create a changeset (do this before every PR)
pnpm changeset

# Apply changesets and bump versions (maintainers only)
pnpm version-packages

Turborepo

The monorepo uses Turborepo for parallel task execution and caching. Most commands are defined in turbo.json and can be run from the repo root via pnpm <script>.

To run a command for a specific package:

pnpm --filter @workspace/<package-name> <command>

Query fan-out access

Query fan-out is an organization add-on, disabled by default in every deployment mode, including WorkOS/local deployments. Platform admins follow the same flag; there is no admin bypass. Everyone in the organization shares its access.

After applying the database migrations through your normal deployment process, run this command from the repository root. It loads .env; an existing DATABASE_URL environment variable takes precedence. Check that it names the intended database before changing customer access.

# Enable for an agency that has purchased the add-on, or your development org
pnpm set-query-fanout --org default --enabled true

# Disable to see the clinic experience, or revoke the add-on
pnpm set-query-fanout --org default --enabled false

--org accepts an organization ID or URL slug. The command creates its settings row if needed and preserves billing settings. It changes only organization_settings.query_fanout_enabled; it does not charge the customer. Pricing and checkout must be agreed before launching paid access.

Reload the app after a toggle to refresh navigation. Server checks use the current flag on each request. When disabled, the fan-out page and data endpoints are blocked, Web Queries are hidden in prompt history and responses, and raw provider payloads are withheld because they can contain embedded searches. Answer text, mentions, and citations remain available. Collection and stored history continue, so enabling the add-on also makes existing query history available.

LLM settings access

The LLMs settings page is disabled by default for every organization, including organizations created on a new Google sign-in. Platform admins follow the same flag. Clients cannot enable it themselves.

From the repository root, use the backend command to grant access only to an organization that should manage its platform choices:

pnpm set-llm-settings --org default --enabled true
pnpm set-llm-settings --org default --enabled false

Like set-query-fanout, this accepts an organization ID or URL slug and loads .env. It updates only organization_settings.llm_settings_enabled, preserving query fan-out access and billing settings. Reload the app after toggling.

When disabled, the sidebar link is hidden, direct page access is blocked, and model-picker reads and saves are rejected server-side. Onboarding skips platform selection and uses the configured defaults; manually supplied platform choices at brand creation are also rejected. Existing tracking keeps running unchanged.

Was this page helpful?