Beeline - Open-source LLM observability and control platform Features: - Real-time agent monitoring dashboard - LLM metrics and analytics (TimescaleDB) - Cost tracking and budget controls - WebSocket event streaming - MCP (Model Context Protocol) server Apache 2.0 License
29 KiB
Developer Guide
This comprehensive guide covers everything you need to know to work on the Beeline monorepo effectively.
Table of Contents
- Repository Overview
- Initial Setup
- Project Structure
- Configuration System
- Development Workflow
- Working with the Frontend (honeycomb)
- Working with the Backend (hive)
- Docker Development
- Testing
- Code Style & Conventions
- Git Workflow
- Debugging
- Common Tasks
- Troubleshooting
Repository Overview
Beeline is a monorepo containing two main packages:
| Package | Directory | Description | Tech Stack |
|---|---|---|---|
| honeycomb | /honeycomb |
Frontend web application | React 18, TypeScript, Vite |
| hive | /hive |
Backend API server | Node.js, Express, TypeScript |
The repository uses npm workspaces to manage dependencies across packages from a single root package.json.
Key Principles
- Single source of configuration: Edit
config.yamlonce, environment files are auto-generated - Consistent tooling: Both packages use TypeScript with strict mode
- Docker-first: Production deployments use containerized builds
- Developer ergonomics: Hot reload, clear error messages, minimal setup
Initial Setup
Prerequisites
Ensure you have installed:
- Node.js v20+ - Download or use nvm:
nvm install 20 - npm v10+ - Comes with Node.js 20
- Docker v20.10+ - Download
- Docker Compose v2+ - Included with Docker Desktop
Verify installation:
node --version # Should be v20.x.x
npm --version # Should be 10.x.x
docker --version # Should be 20.10+
docker compose version # Should be v2.x.x
Step-by-Step Setup
# 1. Clone the repository
git clone https://github.com/adenhq/beeline.git
cd beeline
# 2. Create your configuration file
cp config.yaml.example config.yaml
# 3. (Optional) Edit config.yaml with your settings
# Most defaults work out of the box
# 4. Run the automated setup
npm run setup
The setup script performs these actions:
- Installs all dependencies for root, honeycomb, and hive
- Generates
.envfiles from yourconfig.yaml - Reports any issues
Verify Setup
# Build both packages to verify everything works
npm run build
# Or run in development mode
npm run dev -w honeycomb # Terminal 1: Frontend at http://localhost:3000
npm run dev -w hive # Terminal 2: Backend at http://localhost:4000
Project Structure
beeline/ # Repository root
│
├── .github/ # GitHub configuration
│ ├── workflows/
│ │ ├── ci.yml # Runs on every PR: lint, test, build
│ │ └── release.yml # Runs on tags: publish Docker images
│ ├── ISSUE_TEMPLATE/ # Bug report & feature request templates
│ ├── PULL_REQUEST_TEMPLATE.md # PR description template
│ └── CODEOWNERS # Auto-assign reviewers
│
├── docs/ # Documentation
│ ├── getting-started.md # Quick start guide
│ ├── configuration.md # Configuration reference
│ └── architecture.md # System architecture
│
├── honeycomb/ # FRONTEND PACKAGE
│ ├── src/
│ │ ├── components/ # Reusable UI components
│ │ ├── hooks/ # Custom React hooks
│ │ │ └── useApi.ts # Hook for API calls
│ │ ├── pages/ # Route-level page components
│ │ │ ├── HomePage.tsx
│ │ │ └── NotFoundPage.tsx
│ │ ├── services/ # External service clients
│ │ │ └── api.ts # Backend API client
│ │ ├── styles/ # Global CSS
│ │ │ └── index.css
│ │ ├── types/ # TypeScript type definitions
│ │ │ └── index.ts
│ │ ├── utils/ # Utility functions
│ │ │ └── index.ts
│ │ ├── App.tsx # Root component with routing
│ │ ├── main.tsx # Application entry point
│ │ └── vite-env.d.ts # Vite type declarations
│ ├── public/ # Static assets (copied as-is)
│ │ └── favicon.svg
│ ├── index.html # HTML template
│ ├── nginx.conf # Production nginx config
│ ├── package.json # Package dependencies & scripts
│ ├── tsconfig.json # TypeScript configuration
│ ├── tsconfig.node.json # TypeScript config for Vite
│ ├── vite.config.ts # Vite bundler configuration
│ ├── Dockerfile # Production Docker build
│ ├── Dockerfile.dev # Development Docker build
│ └── .env.example # Environment variable template
│
├── hive/ # BACKEND PACKAGE
│ ├── src/
│ │ ├── config/ # Configuration loading
│ │ │ └── index.ts # Env var parsing & validation
│ │ ├── controllers/ # Request handlers (business logic)
│ │ ├── middleware/ # Express middleware
│ │ │ └── errorHandler.ts # Global error handling
│ │ ├── models/ # Data models / database schemas
│ │ ├── routes/ # API route definitions
│ │ │ ├── api.ts # /api/* routes
│ │ │ └── health.ts # Health check endpoints
│ │ ├── services/ # Business logic services
│ │ ├── types/ # TypeScript type definitions
│ │ │ └── index.ts
│ │ ├── utils/ # Utility functions
│ │ │ └── logger.ts # Structured logging
│ │ ├── index.ts # Application entry point
│ │ └── server.ts # Express server setup
│ ├── package.json # Package dependencies & scripts
│ ├── tsconfig.json # TypeScript configuration
│ ├── Dockerfile # Production Docker build
│ ├── Dockerfile.dev # Development Docker build
│ └── .env.example # Environment variable template
│
├── scripts/ # Build & utility scripts
│ ├── setup.sh # First-time setup script
│ └── generate-env.ts # Generates .env from config.yaml
│
├── config.yaml.example # Configuration template (copy to config.yaml)
├── config.yaml # Your local configuration (git-ignored)
├── docker-compose.yml # Production Docker Compose
├── docker-compose.override.yml.example # Dev overrides template
├── docker-compose.override.yml # Your local dev overrides (git-ignored)
│
├── package.json # Root package.json (workspaces config)
├── package-lock.json # Dependency lock file
├── tsconfig.base.json # Shared TypeScript settings
│
├── .gitignore # Git ignore rules
├── .editorconfig # Editor formatting rules
├── .dockerignore # Docker ignore rules
│
├── README.md # Project overview
├── DEVELOPER.md # This file
├── CONTRIBUTING.md # Contribution guidelines
├── CHANGELOG.md # Version history
├── LICENSE # Apache 2.0 License
├── CODE_OF_CONDUCT.md # Community guidelines
└── SECURITY.md # Security policy
Configuration System
How It Works
Instead of managing multiple .env files, you edit a single config.yaml:
┌─────────────────┐
│ config.yaml │ ← You edit this one file
└────────┬────────┘
│
▼
┌─────────────────┐
│ generate-env.ts │ ← Script transforms YAML to .env
└────────┬────────┘
│
├──────────────────┬──────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ /.env │ │ /honeycomb/.env │ │ /hive/.env │
│ (Docker Compose)│ │ (Frontend) │ │ (Backend) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Configuration Reference
The config.yaml file structure:
# ===========================================
# Application Configuration
# ===========================================
# Application metadata
app:
name: beeline # Used in logs and API responses
environment: development # development | staging | production
# Server configuration
server:
frontend:
port: 3000 # Frontend port
host: "0.0.0.0" # Bind address
backend:
port: 4000 # Backend API port
host: "0.0.0.0" # Bind address
# API configuration
api:
prefix: /api # API route prefix
cors:
origins: # Allowed CORS origins
- "http://localhost:3000"
- "http://localhost:4000"
# Logging configuration
logging:
level: debug # debug | info | warn | error
format: pretty # pretty | json
# Security settings
security:
jwt:
secret: "change-me-in-production-use-min-32-chars"
expiresIn: "7d" # Token expiration
# Database configuration (when needed)
database:
host: localhost
port: 5432
name: beeline
user: postgres
password: postgres
# Feature flags (optional)
features:
enableMetrics: true
enableSwagger: true
Regenerating Environment Files
After editing config.yaml, regenerate the .env files:
npm run generate:env
This is required because:
- Docker Compose reads from
.envfiles - Vite reads frontend env vars from
/honeycomb/.env - Node.js reads backend env vars from
/hive/.env
Development Workflow
Option 1: Local Development (Recommended for Active Development)
Best for rapid iteration with instant hot reload:
# Terminal 1: Start frontend
npm run dev -w honeycomb
# Terminal 2: Start backend
npm run dev -w hive
| Service | URL | Hot Reload |
|---|---|---|
| Frontend | http://localhost:3000 | Yes (Vite HMR) |
| Backend | http://localhost:4000 | Yes (tsx watch) |
| API Health | http://localhost:4000/health | - |
Option 2: Docker Development
Best for testing Docker builds or when you need consistent environments:
# Copy development overrides
cp docker-compose.override.yml.example docker-compose.override.yml
# Start containers with hot reload
docker compose up
# Or in detached mode
docker compose up -d
# View logs
docker compose logs -f
# Stop containers
docker compose down
Option 3: Mixed Mode
Run backend in Docker, frontend locally (useful for frontend-focused work):
# Start only backend in Docker
docker compose up hive -d
# Run frontend locally
npm run dev -w honeycomb
Available NPM Scripts
Root level (run from repository root):
| Command | Description |
|---|---|
npm run setup |
First-time setup (install + generate env) |
npm run generate:env |
Regenerate .env files from config.yaml |
npm run build |
Build all packages |
npm run build -w honeycomb |
Build frontend only |
npm run build -w hive |
Build backend only |
npm run lint |
Lint all packages |
npm run test |
Run all tests |
npm run clean |
Remove node_modules and build artifacts |
Frontend (/honeycomb):
| Command | Description |
|---|---|
npm run dev |
Start Vite dev server with HMR |
npm run build |
Type-check and build for production |
npm run preview |
Preview production build locally |
npm run lint |
Lint with ESLint |
npm run test |
Run tests with Vitest |
npm run test:coverage |
Run tests with coverage report |
Backend (/hive):
| Command | Description |
|---|---|
npm run dev |
Start with hot reload (tsx watch) |
npm run build |
Compile TypeScript to JavaScript |
npm run start |
Run compiled JavaScript |
npm run lint |
Lint with ESLint |
npm run test |
Run tests with Vitest |
npm run test:coverage |
Run tests with coverage report |
Working with the Frontend (honeycomb)
Tech Stack
- React 18 - UI library with hooks
- TypeScript - Type safety
- Vite - Build tool with instant HMR
- React Router v6 - Client-side routing
- Vitest - Testing framework
Adding a New Page
- Create the page component:
// honeycomb/src/pages/UsersPage.tsx
import { useEffect, useState } from 'react';
import { useApi } from '../hooks/useApi';
export function UsersPage() {
const { data, loading, error } = useApi<User[]>('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>Users</h1>
<ul>
{data?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
- Add the route in
App.tsx:
// honeycomb/src/App.tsx
import { UsersPage } from './pages/UsersPage';
// Inside Routes:
<Route path="/users" element={<UsersPage />} />
Adding a New Component
// honeycomb/src/components/Button.tsx
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: 'primary' | 'secondary';
}
export function Button({ children, onClick, variant = 'primary' }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
>
{children}
</button>
);
}
Making API Calls
Use the provided useApi hook or the api service:
// Using the hook (recommended for components)
import { useApi } from '../hooks/useApi';
function MyComponent() {
const { data, loading, error, refetch } = useApi<MyData>('/api/endpoint');
// ...
}
// Using the service directly (for non-component code)
import { api } from '../services/api';
async function fetchData() {
const response = await api.get('/api/endpoint');
return response.data;
}
Environment Variables in Frontend
Access environment variables using import.meta.env:
// Only VITE_* prefixed variables are exposed to the frontend
const apiUrl = import.meta.env.VITE_API_URL;
const appName = import.meta.env.VITE_APP_NAME;
Important: Never put secrets in frontend environment variables. They are bundled into the JavaScript and visible to users.
Path Aliases
Use @/ to import from the src directory:
// Instead of:
import { Button } from '../../../components/Button';
// Use:
import { Button } from '@/components/Button';
Working with the Backend (hive)
Tech Stack
- Node.js 20 - Runtime
- Express - Web framework
- TypeScript - Type safety
- tsx - TypeScript execution with hot reload
- Zod - Runtime validation (recommended)
- Vitest - Testing framework
Adding a New API Endpoint
- Create the route file:
// hive/src/routes/users.ts
import { Router } from 'express';
import type { Request, Response } from 'express';
const router = Router();
// GET /api/users
router.get('/', async (req: Request, res: Response) => {
try {
const users = await getUsersFromDatabase();
res.json(users);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch users' });
}
});
// GET /api/users/:id
router.get('/:id', async (req: Request, res: Response) => {
const { id } = req.params;
try {
const user = await getUserById(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch user' });
}
});
// POST /api/users
router.post('/', async (req: Request, res: Response) => {
const { name, email } = req.body;
try {
const user = await createUser({ name, email });
res.status(201).json(user);
} catch (error) {
res.status(500).json({ error: 'Failed to create user' });
}
});
export default router;
- Register the route in
api.ts:
// hive/src/routes/api.ts
import usersRouter from './users';
// Add to the router:
router.use('/users', usersRouter);
Request Validation with Zod
// hive/src/routes/users.ts
import { z } from 'zod';
const createUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
router.post('/', async (req: Request, res: Response) => {
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
details: result.error.issues
});
}
const { name, email, age } = result.data;
// ... create user
});
Adding Middleware
// hive/src/middleware/auth.ts
import type { Request, Response, NextFunction } from 'express';
export function requireAuth(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}
// Usage in routes:
router.get('/protected', requireAuth, (req, res) => {
res.json({ user: req.user });
});
Logging
Use the built-in logger for consistent structured logging:
import { logger } from '../utils/logger';
// Different log levels
logger.debug('Detailed debug info', { userId: 123 });
logger.info('User logged in', { userId: 123 });
logger.warn('Rate limit approaching', { currentRate: 95 });
logger.error('Database connection failed', { error: err.message });
Environment Variables in Backend
Access via process.env or the config module:
// Direct access
const port = process.env.PORT || 4000;
// Or via config (recommended - adds validation)
import { config } from '../config';
const port = config.port;
Docker Development
Docker Compose Files
| File | Purpose |
|---|---|
docker-compose.yml |
Base configuration (production-like) |
docker-compose.override.yml |
Development overrides (hot reload, debug ports) |
When you run docker compose up, Docker automatically merges both files.
Building Images
# Build all images
docker compose build
# Build specific service
docker compose build honeycomb
docker compose build hive
# Build with no cache (fresh build)
docker compose build --no-cache
Running Containers
# Start all services
docker compose up
# Start in background
docker compose up -d
# Start specific service
docker compose up hive
# View logs
docker compose logs -f
docker compose logs -f hive # Specific service
# Stop all services
docker compose down
# Stop and remove volumes
docker compose down -v
Debugging in Docker
The development override exposes debug ports:
- Backend debug port: 9229 (Node.js inspector)
To debug the backend in VS Code:
- Add to
.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Docker",
"type": "node",
"request": "attach",
"port": 9229,
"address": "localhost",
"localRoot": "${workspaceFolder}/hive",
"remoteRoot": "/app",
"restart": true
}
]
}
- Start containers:
docker compose up - In VS Code, press F5 or select "Attach to Docker"
Useful Docker Commands
# Execute command in running container
docker compose exec hive sh
docker compose exec honeycomb sh
# View container resource usage
docker stats
# Remove all stopped containers
docker container prune
# Remove unused images
docker image prune
Testing
Running Tests
# Run all tests
npm run test
# Run tests for specific package
npm run test -w honeycomb
npm run test -w hive
# Run with coverage
npm run test:coverage -w honeycomb
npm run test:coverage -w hive
# Run in watch mode (re-runs on file changes)
cd honeycomb && npm run test -- --watch
cd hive && npm run test -- --watch
Writing Frontend Tests
// honeycomb/src/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Button } from './Button';
describe('Button', () => {
it('renders children', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
Writing Backend Tests
// hive/src/routes/health.test.ts
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import { app } from '../server';
describe('Health Routes', () => {
it('GET /health returns healthy status', async () => {
const response = await request(app).get('/health');
expect(response.status).toBe(200);
expect(response.body).toMatchObject({
status: 'healthy',
});
});
it('GET /health/ready returns ready status', async () => {
const response = await request(app).get('/health/ready');
expect(response.status).toBe(200);
expect(response.body.ready).toBe(true);
});
});
Code Style & Conventions
TypeScript
- Strict mode enabled - No implicit any, strict null checks
- Explicit return types on exported functions
- Interface over type for object shapes (unless unions needed)
- Readonly where possible
// Good
interface User {
readonly id: string;
name: string;
email: string;
}
export function getUser(id: string): Promise<User | null> {
// ...
}
// Avoid
export function getUser(id) { // Missing types
// ...
}
React Components
- Functional components only (no class components)
- Named exports for components
- Props interface defined above component
// Good
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
}
export function Button({ children, onClick }: ButtonProps) {
return <button onClick={onClick}>{children}</button>;
}
// Avoid
export default function({ children, onClick }) { // Missing types, default export
return <button onClick={onClick}>{children}</button>;
}
File Naming
| Type | Convention | Example |
|---|---|---|
| Components | PascalCase | UserCard.tsx |
| Hooks | camelCase with use prefix |
useAuth.ts |
| Utilities | camelCase | formatDate.ts |
| Types | PascalCase | User.ts or in types/index.ts |
| Tests | Same as file + .test |
UserCard.test.tsx |
| Styles | Same as component | UserCard.css |
Import Order
- External packages
- Internal absolute imports (
@/...) - Relative imports
- Style imports
// External
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
// Internal absolute
import { Button } from '@/components/Button';
import { useApi } from '@/hooks/useApi';
// Relative
import { formatUserName } from './utils';
// Styles
import './UserCard.css';
Git Workflow
Branch Naming
feature/add-user-authentication
bugfix/fix-login-redirect
hotfix/security-patch
chore/update-dependencies
docs/improve-readme
Commit Messages
Follow Conventional Commits:
<type>(<scope>): <description>
[optional body]
[optional footer]
Types:
feat- New featurefix- Bug fixdocs- Documentation onlystyle- Formatting, missing semicolons, etc.refactor- Code change that neither fixes a bug nor adds a featuretest- Adding or updating testschore- Maintenance tasks
Examples:
feat(auth): add JWT authentication
fix(api): handle null response from external service
docs(readme): update installation instructions
chore(deps): update React to 18.2.0
Pull Request Process
- Create a feature branch from
main - Make your changes with clear commits
- Run tests locally:
npm run test - Run linting:
npm run lint - Push and create a PR
- Fill out the PR template
- Request review from CODEOWNERS
- Address feedback
- Squash and merge when approved
Debugging
Frontend Debugging
React Developer Tools:
- Install the React DevTools browser extension
- Open browser DevTools → React tab
- Inspect component tree, props, state, and hooks
VS Code Debugging:
- Add Chrome debug configuration to
.vscode/launch.json:
{
"type": "chrome",
"request": "launch",
"name": "Debug Frontend",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/honeycomb/src"
}
- Start the dev server:
npm run dev -w honeycomb - Press F5 in VS Code
Backend Debugging
VS Code Debugging:
- Add Node debug configuration:
{
"type": "node",
"request": "launch",
"name": "Debug Backend",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"cwd": "${workspaceFolder}/hive",
"console": "integratedTerminal"
}
- Set breakpoints in your code
- Press F5 to start debugging
Logging:
import { logger } from '../utils/logger';
// Add debug logs
logger.debug('Processing request', {
userId: req.user.id,
body: req.body
});
Common Tasks
Adding a New Dependency
# Add to frontend
npm install <package> -w honeycomb
# Add to backend
npm install <package> -w hive
# Add dev dependency
npm install -D <package> -w honeycomb
# Add to root (shared tooling)
npm install -D <package> -w .
Updating Dependencies
# Check for outdated packages
npm outdated
# Update all to latest minor/patch
npm update
# Update specific package
npm install <package>@latest -w honeycomb
Adding Environment Variables
- Add to
config.yaml.example(template):
myService:
apiKey: "your-api-key-here"
- Add to your local
config.yaml:
myService:
apiKey: "actual-api-key"
-
Update
scripts/generate-env.tsto output the new variable -
Regenerate env files:
npm run generate:env
- Access in code:
// Backend
const apiKey = process.env.MY_SERVICE_API_KEY;
// Frontend (must be prefixed with VITE_)
const apiKey = import.meta.env.VITE_MY_SERVICE_API_KEY;
Database Migrations (when added)
# Create a new migration
npm run migration:create -w hive -- --name add-users-table
# Run pending migrations
npm run migration:run -w hive
# Rollback last migration
npm run migration:rollback -w hive
Troubleshooting
Port Already in Use
# Find process using port
lsof -i :3000
lsof -i :4000
# Kill process
kill -9 <PID>
# Or change ports in config.yaml and regenerate
Node Modules Issues
# Clean everything and reinstall
npm run clean
rm -rf node_modules package-lock.json
npm install
Docker Issues
# Reset Docker state
docker compose down -v
docker system prune -f
docker compose build --no-cache
docker compose up
TypeScript Errors After Pull
# Rebuild TypeScript
npm run build
# Or restart TS server in VS Code
# Cmd/Ctrl + Shift + P → "TypeScript: Restart TS Server"
Environment Variables Not Loading
# Regenerate from config.yaml
npm run generate:env
# Verify files exist
cat .env
cat honeycomb/.env
cat hive/.env
# Restart dev servers after changing env
Tests Failing
# Run with verbose output
npm run test -w honeycomb -- --reporter=verbose
# Run single test file
npm run test -w honeycomb -- src/components/Button.test.tsx
# Clear test cache
npm run test -w honeycomb -- --clearCache
Getting Help
- Documentation: Check the
/docsfolder - Issues: Search existing issues
- Discord: Join our community
- Code Review: Tag a maintainer on your PR
Happy coding! 🐝