cavendo.com · Product Page · Documentation
Beta / Early Release — Cavendo Engine is under active development. APIs, schema, and behavior may change between releases. Suitable for evaluation, development, and early adoption. Not yet recommended for mission-critical production workloads without testing.
Cavendo Engine provides the infrastructure for AI agents to receive tasks, submit deliverables, and get human feedback in a structured review loop.
- User Management - Create users with auto-linked human agents for task assignment
- Agent Registration - Register agents with API keys and scoped permissions
- Agent Profiles - Rich agent metadata with capabilities, specializations, and capacity limits
- Task Routing - Automatic task assignment based on tags, priority, and agent capabilities
- User API Keys - Personal
cav_uk_keys for MCP access as yourself - Agent Execution - Execute tasks via Anthropic, OpenAI, or any OpenAI-compatible endpoint (Ollama, LM Studio, vLLM)
- Task Management - Create, assign, and track tasks for agents
- Sprint Planning - Organize tasks into sprints/milestones with progress tracking
- Task Claiming - Agents can self-assign tasks from a pool
- Progress Logging - Track task progress with percentage and details
- Deliverable Workflow - Submit → Review → Approve/Revise/Reject cycle
- Delivery Routes - Auto-route approved content to webhooks/email (with pluggable destinations)
- Token Usage Tracking - Track input/output tokens for AI-generated deliverables
- Comments/Discussion - Threaded discussions on tasks and deliverables
- Bulk Operations - Create, update, or delete up to 100 tasks at once
- Agent Metrics - Performance analytics: approval rates, completion times
- Outbound Webhooks - 20 event types with HMAC signing and retry logic
- S3 Storage Routes - Auto-upload deliverables to S3-compatible storage (AWS, MinIO, Backblaze B2)
- Email Notifications - Multi-provider (SMTP, SendGrid, Mailjet, Postmark, SES)
- Universal Activity Log - Full audit trail for deliverable and task lifecycle events
- External Employees - First-class Bring Your Own Agent runtimes with queue polling, lease claiming, heartbeat, and result submission
- Context - Project context for agent task completion
- Minimal Admin UI - React-based dashboard for human oversight
- Node.js 18.0 or higher
- npm or yarn
- Python 3.x with setuptools (for native module compilation)
- macOS:
brew install python-setuptools - Linux:
pip install setuptoolsor install via package manager - Windows: Usually included with Python installer
- macOS:
git clone https://github.com/Cavendo/Engine.git Cavendo-Engine
cd Cavendo-Engine
# Install server and UI dependencies
npm install
cd ui && npm install && cd ..
# Start development server (API + UI with hot reload)
npm run devOn first start, the server automatically:
- Generates
.envwith secure random secrets (JWT, encryption key) - Creates the SQLite database and applies the schema
- Seeds a default admin user
The API runs at http://localhost:3001 and the UI dev server at http://localhost:5173.
For production, build the UI first: npm run build, then npm start.
Development-only bootstrap credentials:
- Email:
admin@cavendo.local - Password:
admin
They are forced through a password change before they can use the API. Production startup refuses this account and requires BOOTSTRAP_ADMIN_EMAIL plus BOOTSTRAP_ADMIN_PASSWORD (or BOOTSTRAP_ADMIN_PASSWORD_FILE) when no administrator exists.
Two key types are supported for API and MCP access.
You can send the key in any of these headers:
X-Agent-Key: <your-api-key>X-API-Key: <your-api-key>Authorization: Bearer <your-api-key>
Note: Browser-based API calls (POST, PATCH, PUT, DELETE) require the X-CSRF-Token header with the token returned from the login response. API key authentication bypasses CSRF checks.
| Key Type | Format | Identity | Use Case |
|---|---|---|---|
| User Key | cav_uk_... |
Acts as the user | Personal MCP access, direct user API access |
| Agent Key | cav_ak_... |
Agent identity | Automated bots, worker/MCP access |
# User key - acts as you
curl -H "X-Agent-Key: cav_uk_..." http://localhost:3001/api/agents/me/tasks
# Agent key - acts as the agent
curl -H "X-Agent-Key: cav_ak_..." http://localhost:3001/api/agents/me/tasks
# Bearer auth is also supported
curl -H "Authorization: Bearer cav_uk_..." http://localhost:3001/api/agents/meFor internal service-to-service provisioning, use a bearer token instead of X-Agent-Key:
openssl rand -hex 32INTERNAL_SERVICE_TOKEN=replace_with_random_secretAuthorization: Bearer <INTERNAL_SERVICE_TOKEN>
X-Internal-Service-Name: workflow_engineCavendo supports first-class external employees that run outside the built-in executor. These workers:
- poll for assigned work through the API
- claim an execution lease before starting
- fetch the full task context bundle
- renew the lease with heartbeat calls while running
- publish lifecycle state with
external-status - submit results back into Cavendo for review
Recommended usage:
- use agent keys for unattended workers
- use user keys for user-owned local assistants and MCP sessions
- use the HTTP API for long-running queue execution
- use MCP for interactive, local, assistant-style access
See docs/external-agents.md for the full runtime contract.
Two important rules:
- tasks are assigned to
agents, not directly tousers - some OAuth/callback-based connector handshakes can still require an interactive browser finish step
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/users |
Create user (auto-creates linked agent) |
| PATCH | /api/users/:id |
Update user (cascades to agent) |
| DELETE | /api/users/:id |
Delete user and linked agent |
| POST | /api/agents |
Register new agent |
| GET | /api/agents/:id |
Get agent details |
| PATCH | /api/agents/:id |
Update agent |
| DELETE | /api/agents/:id |
Delete agent |
| POST | /api/agents/:id/keys |
Generate agent API key |
| DELETE | /api/agents/:id/keys/:keyId |
Revoke agent API key |
| GET | /api/agents/:id/metrics |
Get agent performance metrics |
| GET | /api/agents/providers |
List supported AI providers |
| POST | /api/agents/match |
Match agents for a task payload |
| POST | /api/agents/:id/execute |
Execute task via provider API |
| POST | /api/agents/:id/test-connection |
Validate provider configuration |
| PATCH | /api/agents/:id/execution |
Update execution/provider settings |
| GET | /api/users/me/keys |
List personal API keys |
| POST | /api/users/me/keys |
Generate personal API key |
| GET | /api/agents/me/tasks |
List assigned tasks |
| GET | /api/agents/me/tasks/next |
Get next task from queue |
| POST | /api/agents/me/tasks/poll |
Poll assigned work for an external employee |
| POST | /api/agents/me/tasks/:taskId/claim |
Claim an execution lease for assigned work |
| POST | /api/agents/me/tasks/:taskId/heartbeat |
Renew a claimed external execution lease |
| POST | /api/agents/me/tasks/:taskId/release |
Release a claimed external execution lease |
| POST | /api/tasks/:id/claim |
Claim unassigned task |
| POST | /api/tasks/:id/progress |
Log progress update |
| GET | /api/tasks/:id/context |
Get full task context bundle |
| POST | /api/tasks/:id/external-status |
Publish external execution lifecycle state |
| POST | /api/tasks/:id/result |
Submit an external worker result |
| PATCH | /api/tasks/:id |
Update task fields |
| POST | /api/tasks/bulk |
Bulk create tasks (up to 50) |
| PATCH | /api/tasks/bulk |
Bulk update tasks |
| DELETE | /api/tasks/bulk |
Bulk delete tasks |
| DELETE | /api/tasks/:id |
Delete task |
| POST | /api/tasks/:id/execute |
Force task execution |
| POST | /api/tasks/:id/retry |
Retry failed task execution |
| GET | /api/sprints |
List sprints |
| POST | /api/sprints |
Create sprint |
| GET | /api/sprints/:id/tasks |
Get tasks in sprint |
| POST | /api/deliverables |
Submit deliverable |
| PATCH | /api/deliverables/:id/review |
Approve/revise/reject |
| GET | /api/deliverables/:id/feedback |
Get revision feedback |
| POST | /api/tasks/:id/comments |
Add comment to task |
| POST | /api/deliverables/:id/comments |
Add comment to deliverable |
| POST | /api/projects/:id/routes |
Create delivery route |
| POST | /api/projects |
Create project |
| PATCH | /api/projects/:id |
Update project |
| DELETE | /api/projects/:id |
Delete project |
| GET | /api/projects/:id/routing-rules |
Get project routing rules |
| PUT | /api/projects/:id/routing-rules |
Update project routing rules |
| POST | /api/projects/:id/routing-rules/test |
Test routing rules |
| GET | /api/skills-runtime/catalog |
List runtime skills allowed for actor |
| POST | /api/skills-runtime/invocations |
Invoke runtime skill |
| GET | /api/skills-runtime/invocations/:id |
Get runtime skill invocation |
| POST | /api/skills-runtime/invocations/:id/cancel |
Cancel runtime skill invocation |
| GET | /api/routes/:id/logs |
View delivery log |
| POST | /api/routes/:id/test |
Test route connection |
| GET | /api/deliverables/:id/activity |
Deliverable activity timeline |
| GET | /api/tasks/:id/activity |
Task activity timeline |
| GET | /api/activity/entity/:type/:id |
Activity log by entity type |
| POST | /api/internal/provisioning/projects/ensure |
Ensure project by external key (internal token) |
| POST | /api/internal/provisioning/projects/:externalKey/routing-rules/ensure |
Ensure task routing config (internal token) |
These endpoints are backend-only. They do not accept browser session auth or X-Agent-Key, and are intended for automation that needs stable idempotent project provisioning.
curl -X POST http://localhost:3001/api/internal/provisioning/projects/ensure \
-H "Authorization: Bearer $INTERNAL_SERVICE_TOKEN" \
-H "X-Internal-Service-Name: workflow_engine" \
-H "Content-Type: application/json" \
-d '{
"externalKey": "project:acme",
"name": "Acme",
"description": "Provisioned by workflow automation",
"status": "active"
}'Configure webhooks per-agent to receive real-time notifications:
deliverable.approved- Deliverable approveddeliverable.submitted- New deliverable submitteddeliverable.revision_requested- Revision requested with feedbackdeliverable.rejected- Deliverable rejectedtask.created- New task createdtask.assigned- Task assigned to agenttask.completed- Task completedtask.status_changed- Task status transitionstask.overdue- Task past due datetask.updated- Task details changedtask.claimed- Agent claimed a tasktask.progress_updated- Progress logged on tasktask.routing_failed- No matching agent for routingtask.execution_failed- Agent execution failedreview.completed- Review action taken on deliverableagent.registered- New agent createdagent.status_changed- Agent status changedproject.created- New project createdproject.knowledge_updated- Project knowledge base changedknowledge.updated- Knowledge entry changed
Delivery routes automatically push approved content to external systems. Configure per-project routes that trigger on events like deliverable.approved:
# Create a webhook route
curl -X POST http://localhost:3001/api/projects/1/routes \
-H "Content-Type: application/json" \
-H "Cookie: session=..." \
-H "X-CSRF-Token: ..." \
-d '{
"name": "Notify on approval",
"trigger_event": "deliverable.approved",
"destination_type": "webhook",
"destination_config": {
"url": "https://example.com/webhook",
"signing_secret": "whsec_..."
}
}'Destination Types:
webhook- POST to any URL with optional HMAC signingemail- Send via configured provider (SMTP, SendGrid, Mailjet, Postmark, SES)storage- Upload deliverable files to S3-compatible storage (AWS S3, MinIO, Backblaze B2)
Email Configuration:
Email for delivery routes is configured via environment variables in .env. Set EMAIL_PROVIDER to one of the supported providers and add the corresponding credentials:
| Provider | EMAIL_PROVIDER |
Required Variables |
|---|---|---|
| SMTP | smtp (default) |
EMAIL_SMTP_HOST, EMAIL_SMTP_PORT (default 587), EMAIL_SMTP_SECURE (true/false), EMAIL_SMTP_USER, EMAIL_SMTP_PASS |
| SendGrid | sendgrid |
EMAIL_SENDGRID_API_KEY |
| Mailjet | mailjet |
EMAIL_MAILJET_API_KEY, EMAIL_MAILJET_SECRET_KEY |
| Postmark | postmark |
EMAIL_POSTMARK_SERVER_TOKEN |
| AWS SES | ses |
EMAIL_SES_REGION, EMAIL_SES_ACCESS_KEY_ID, EMAIL_SES_SECRET_ACCESS_KEY |
Common variables (all providers):
EMAIL_FROM— Sender email address (default:notifications@cavendo.local)EMAIL_FROM_NAME— Sender display name (default:Cavendo)
Example (SendGrid):
EMAIL_PROVIDER=sendgrid
EMAIL_SENDGRID_API_KEY=SG.xxx
EMAIL_FROM=notifications@example.com
EMAIL_FROM_NAME=CavendoExample (SMTP):
EMAIL_PROVIDER=smtp
EMAIL_SMTP_HOST=smtp.example.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_SECURE=false
EMAIL_SMTP_USER=user@example.com
EMAIL_SMTP_PASS=secret
EMAIL_FROM=notifications@example.comStorage Route Configuration:
# Create an S3 storage route
curl -X POST http://localhost:3001/api/projects/1/routes \
-H "Content-Type: application/json" \
-H "Cookie: session=..." \
-H "X-CSRF-Token: ..." \
-d '{
"name": "Archive approved deliverables",
"trigger_event": "deliverable.approved",
"destination_type": "storage",
"destination_config": {
"provider": "s3",
"bucket": "my-deliverables",
"region": "us-east-1",
"access_key_id": "AKIA...",
"secret_access_key": "...",
"path_prefix": "cavendo/",
"upload_content": true,
"upload_files": true
}
}'For MinIO or other S3-compatible services, add the endpoint field:
{
"endpoint": "https://minio.example.com:9000"
}See Routes API for full documentation.
| Package | Path | Audience | Stability | Description |
|---|---|---|---|---|
| @cavendo/mcp-server | packages/mcp-server |
MCP-native tools (Claude Code, Claude Desktop, etc.) | Stable | MCP server providing tools for task management, deliverable submission, and knowledge access |
| cavendo (Python SDK) | packages/python-sdk |
Python developers building agents or automation | Beta | Typed Python client wrapping the full REST API |
| OpenClaw Skill | packages/openclaw-skill |
OpenClaw agent framework users | Beta | CLI scripts (check_tasks, submit_work, get_context) for shell-based agent workflows |
Each package is independently versioned. See individual CHANGELOG.md files in each package directory.
npm install @cavendo/mcp-serverConfigure in Claude Desktop with your personal API key:
{
"mcpServers": {
"cavendo": {
"command": "npx",
"args": ["@cavendo/mcp-server"],
"env": {
"CAVENDO_URL": "http://localhost:3001",
"CAVENDO_AGENT_KEY": "cav_uk_..."
}
}
}
}Or use an agent key for bot identity:
{
"mcpServers": {
"cavendo": {
"command": "npx",
"args": ["@cavendo/mcp-server"],
"env": {
"CAVENDO_URL": "http://localhost:3001",
"CAVENDO_AGENT_KEY": "cav_ak_..."
}
}
}
}Agents can be configured to execute tasks via AI provider APIs (Anthropic, OpenAI, or any OpenAI-compatible endpoint). Provider API keys are stored encrypted (AES-256-GCM).
| Mode | Behavior |
|---|---|
| Manual | Admin triggers execution via UI or API |
| Auto | Task Dispatcher automatically executes when tasks are assigned |
| Polling | Agent periodically checks for new tasks (external agent) |
| Human | Human worker — dispatcher skips, notifications via delivery routes |
The built-in Task Dispatcher is a background service that automatically executes tasks assigned to agents with execution_mode = 'auto'. It runs as part of the server process.
How it works:
- Polls for eligible tasks on a configurable interval (default: 30 seconds)
- Checks agent capacity (
active_task_count < max_concurrent_tasks) - Gathers full task context (project context, previous deliverables, feedback)
- Executes via the agent's configured AI provider
- Creates a deliverable from the response and sets task to
review - Handles revision chains automatically — when re-executing a task sent back for revision, links the new deliverable to the previous version via
parent_idand marks the old one asrevised
Configuration (.env):
DISPATCHER_INTERVAL_MS=30000 # Polling interval (default: 30 seconds)
DISPATCHER_BATCH_SIZE=5 # Max tasks per cycle (default: 5)Error handling:
- Errors are categorized with appropriate cooldowns:
auth_error/config_error(5 min),timeout/overloaded(10 min),rate_limited(60 min),unknown(6 hours) - Updating an agent's API key automatically clears stuck task errors
- Manual retry:
POST /api/tasks/:id/retryclears a task's error cache - All executions are logged to the Activity Log for visibility
Manual execution:
# Execute a specific task now (requires session auth + CSRF token)
curl -X POST \
-H "Cookie: session=YOUR_SESSION_ID" \
-H "X-CSRF-Token: YOUR_CSRF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"taskId": 123}' \
http://localhost:3001/api/agents/1/execute
# Test provider connection
curl -X POST \
-H "Cookie: session=YOUR_SESSION_ID" \
-H "X-CSRF-Token: YOUR_CSRF_TOKEN" \
http://localhost:3001/api/agents/1/test-connectionUse the openai_compatible provider to run tasks against Ollama, LM Studio, vLLM, or any server exposing /v1/chat/completions:
# Configure an agent for local Ollama
curl -X PATCH \
-H "Cookie: session=YOUR_SESSION_ID" \
-H "X-CSRF-Token: YOUR_CSRF_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"provider": "openai_compatible",
"providerModel": "llama3.2:latest",
"providerBaseUrl": "http://localhost:11434",
"providerLabel": "Ollama",
"executionMode": "manual"
}' \
http://localhost:3001/api/agents/1/executionKnown gotcha: The model name must match the exact tag in your local runtime (e.g.,
ollama list). A typo likellama3.2instead ofllama3.2:latestwill return a model-not-found error from the local server.
Endpoint security — By default, only local and allowlisted endpoints are permitted. Configure via .env:
# Allow remote HTTPS endpoints (default: false)
ALLOW_CUSTOM_PROVIDER_BASE_URLS=true
# Allowlist specific hosts (comma-separated, supports host:port)
PROVIDER_BASE_URL_ALLOWLIST=gpu-box.lan,myserver.local:11434
# Default base URL when agent has none set (default: http://localhost:11434)
OPENAI_COMPAT_DEFAULT_BASE_URL=http://localhost:11434When a deliverable is sent back for revision and re-executed (by the dispatcher or manually), the system maintains a version chain:
v1 (revised) ← v2 (revised) ← v3 (pending)
↑ parent_id ↑ parent_id
Each deliverable has a parent_id linking to its previous version and an incrementing version number. The GET /api/deliverables/:id response includes a versions array showing the full history.
pip install cavendo-enginefrom cavendo import CavendoClient
client = CavendoClient(
url="http://localhost:3001",
api_key="cav_ak_..."
)
task = client.tasks.next()
if task:
client.deliverables.submit(
task_id=task.id, # Task is a typed object, use dot notation
title="Research Report",
content="## Findings\n..."
)AGPL-3.0 - See LICENSE for details.
For alternative licensing arrangements, contact sales@cavendo.com.
Cavendo Engine uses SQLite for simplicity. The database is automatically created on first run.
# Initialize fresh database
npm run db:init
# Database location
data/cavendo.dbserver/db/schema.sql is the canonical v0.1.0 baseline schema. On fresh installs, db:init runs this file directly — no migrations are needed. Future releases will include a migrations/ directory for post-v0.1.0 schema upgrades.
All configuration is via environment variables in .env. See .env.example for a ready-to-copy template.
Production security: Always set strong, unique values for
JWT_SECRET,SESSION_SECRET,SESSION_HASH_SECRET, and encryption key material (ENCRYPTION_KEYorENCRYPTION_KEYRING). SetBOOTSTRAP_ADMIN_EMAILandBOOTSTRAP_ADMIN_PASSWORD(orBOOTSTRAP_ADMIN_PASSWORD_FILE) for a fresh production deployment. IfSESSION_SECRETis not set, the server falls back toJWT_SECRETfor key derivation; session token hashing falls back through those secrets and then stable encryption key material. Set these explicitly in production to separate concerns.
| Variable | Purpose | Default | Required |
|---|---|---|---|
PORT |
Server listen port | 3001 |
No |
NODE_ENV |
Environment (development / production) |
development |
Yes (prod) |
DATABASE_PATH |
SQLite database file path | ./data/cavendo.db |
No |
CORS_ORIGIN |
Allowed CORS origin for UI | http://localhost:5173 |
No |
TRUST_PROXY |
Set true or proxy count when behind reverse proxy |
— | No |
| Variable | Purpose | Default | Required |
|---|---|---|---|
JWT_SECRET |
Server secret used as fallback entropy for encryption key derivation | Auto-generated on first run | Yes (prod) |
SESSION_SECRET |
Preferred secret for encryption key derivation (takes priority over JWT_SECRET) |
Falls back to JWT_SECRET |
Recommended (prod) |
SESSION_HASH_SECRET |
Dedicated HMAC secret for stored session token hashes | Falls back to session/JWT/encryption key material | Recommended (prod) |
BOOTSTRAP_ADMIN_EMAIL |
Initial administrator email when no admin exists | — | Yes (fresh prod install) |
BOOTSTRAP_ADMIN_PASSWORD / _FILE |
Initial administrator password (minimum 14 characters) | — | Yes (fresh prod install) |
API_KEY_PREFIX |
Prefix for agent API keys | cav_ak |
No |
RATE_LIMIT_API |
Pre-auth API requests per minute, keyed by resolved client IP | 300 |
No |
RATE_LIMIT_AUTHENTICATED |
Post-auth API requests per minute, keyed by verified user or agent identity | 3000 |
No |
RATE_LIMIT_API_ALLOWLIST |
Comma-separated client IPs or CIDR ranges that bypass the general API limiter | — | No |
RATE_LIMIT_APIis intentionally pre-auth and IP-only. Authenticated routes also applyRATE_LIMIT_AUTHENTICATEDafter a user or agent key has been verified.RATE_LIMIT_API_ALLOWLISTuses the resolved client IP, so setTRUST_PROXYcorrectly when running behind nginx, Cloudflare, or another reverse proxy; otherwise all clients can appear as the proxy address and share one bucket. When proxy trust is enabled, Cavendo honorsCF-Connecting-IP,True-Client-IP, andX-Forwarded-Forin that order. Example:RATE_LIMIT_API_ALLOWLIST=203.0.113.10,198.51.100.0/24
| Variable | Purpose | Default | Required |
|---|---|---|---|
ENCRYPTION_KEY |
AES-256 key for provider API key encryption | Derived from session secret (dev only) | Yes (prod) |
ENCRYPTION_SALT |
Salt for key derivation | cavendo-dev-salt |
Yes (prod) |
ENCRYPTION_KEYRING |
JSON keyring for key rotation (overrides above) | — | No |
ENCRYPTION_KEY_VERSION_CURRENT |
Active keyring version | Latest in keyring | No |
| Variable | Purpose | Default | Required |
|---|---|---|---|
DISPATCHER_INTERVAL_MS |
Auto-execution polling interval | 30000 (30s) |
No |
DISPATCHER_BATCH_SIZE |
Max tasks to execute per cycle | 5 |
No |
EXECUTION_TIMEOUT_MS |
Provider API call timeout | 120000 (2 min) |
No |
| Variable | Purpose | Default | Required |
|---|---|---|---|
WEBHOOK_TIMEOUT_MS |
Webhook delivery timeout | 5000 (5s) |
No |
WEBHOOK_MAX_RETRIES |
Max webhook retry attempts | 3 |
No |
ALLOW_PRIVATE_WEBHOOKS |
Allow localhost/private IP webhook URLs | false |
No (dev only) |
RETRY_SWEEP_INTERVAL_MS |
Failed delivery retry sweep interval | 15000 (15s) |
No |
| Variable | Purpose | Default | Required |
|---|---|---|---|
ALLOW_CUSTOM_PROVIDER_BASE_URLS |
Allow remote HTTPS provider endpoints | false |
No |
PROVIDER_BASE_URL_ALLOWLIST |
Comma-separated trusted hosts (e.g., gpu-box.lan:11434) |
— | No |
OPENAI_COMPAT_DEFAULT_BASE_URL |
Fallback base URL for openai_compatible agents |
http://localhost:11434 |
No |
| Variable | Purpose | Default | Required |
|---|---|---|---|
EMAIL_PROVIDER |
Provider: smtp, sendgrid, mailjet, postmark, ses |
smtp |
No |
EMAIL_FROM |
Sender email address | notifications@cavendo.local |
No |
EMAIL_FROM_NAME |
Sender display name | Cavendo |
No |
EMAIL_SMTP_HOST |
SMTP server hostname | — | If using SMTP |
EMAIL_SMTP_PORT |
SMTP server port | 587 |
No |
EMAIL_SMTP_SECURE |
Use TLS (true/false) |
false |
No |
EMAIL_SMTP_USER |
SMTP username | — | If using SMTP auth |
EMAIL_SMTP_PASS |
SMTP password | — | If using SMTP auth |
EMAIL_SENDGRID_API_KEY |
SendGrid API key | — | If using SendGrid |
EMAIL_MAILJET_API_KEY |
Mailjet API key | — | If using Mailjet |
EMAIL_MAILJET_SECRET_KEY |
Mailjet secret key | — | If using Mailjet |
EMAIL_POSTMARK_SERVER_TOKEN |
Postmark server token | — | If using Postmark |
EMAIL_SES_REGION |
AWS SES region | — | If using SES |
EMAIL_SES_ACCESS_KEY_ID |
AWS access key ID | — | If using SES |
EMAIL_SES_SECRET_ACCESS_KEY |
AWS secret access key | — | If using SES |
Maintenance note: When adding new
process.env.*usage in server code, update both this table and.env.example.
If you see ModuleNotFoundError: No module named 'distutils' during npm install:
# macOS
brew install python-setuptools
# Linux/Windows
pip install setuptoolsThen retry npm install.
Cavendo Engine exports a createApp() factory from server/app.js for embedding the engine in a larger application (e.g., layering commercial routes on top).
import { createApp } from './server/app.js';
const { app, start, stop } = createApp({
// Runs before engine routes are mounted
async beforeRoutes(app) {
app.use(myCustomMiddleware());
},
// Runs after engine routes but before SPA fallback and error handlers
async afterRoutes(app) {
app.get('/api/custom-endpoint', (req, res) => res.json({ ok: true }));
},
// Runs after DB init/migrations but before HTTP listen
async beforeStart(app) {
await seedExtraData();
},
// Runs after server is listening — fatal if it throws
async onStarted({ app, server }) {
console.log('Custom startup complete');
},
});
await start({ port: 3002, host: '0.0.0.0' });
// Graceful shutdown: closes server, stops dispatcher/retry workers, closes DB
await stop();All hooks are optional and support both sync and async functions.
Standalone mode — node server/index.js (or npm start) uses the same factory internally with no hooks, preserving existing behavior.
Known limitation: stop() closes the SQLite connection, which is terminal. Calling start() after stop() in the same process will throw. To restart, create a new process.
server/env.js is imported at the top of app.js and runs at import time. It auto-generates .env from .env.example on first run (with random secrets) and loads existing .env values into process.env. This ensures env vars are available before any other module (DB, routes, services) resolves.
Future storage integrations planned:
- Google Drive (OAuth2)
- Microsoft OneDrive (OAuth2)
- Dropbox (OAuth2)
Contributions welcome! Please read our contributing guidelines before submitting PRs.
- Website: cavendo.com
- Product Page: cavendo.com/engine
- Documentation: docs/
- Issues: GitHub Issues
- License: AGPL-3.0 — for alternative licensing, contact sales@cavendo.com
