Open-source observability for traces, logs & metrics, built on OpenTelemetry and ClickHouse.
Website · Docs · Run locally · Changelog · Brand
Why Maple
- OpenTelemetry native. Point any OTLP exporter at Maple. No proprietary agent, no vendor SDK lock-in.
- Traces, logs and metrics in one place. Waterfalls, log search, metric explorers and dashboards share the same filters and time range.
- Built for agents. A hosted MCP server lets Claude Code, Cursor and other clients query your telemetry, and investigations run an agent over it for you.
- Errors, alerts and replays. Error issues with a triage lifecycle, alert rules that notify PagerDuty, Discord, Telegram, email or a webhook, and browser session replay.
- Runs where you want. Use maple.dev, self-host the stack, or run everything as one local binary.
Screenshots
![]() Traces. Span waterfall with attributes, logs and infrastructure per span. |
![]() Service map. Live dependencies with request rate, error rate and latency on every edge. |
![]() Logs. Search and facet logs, jump straight to the trace that emitted them. |
![]() Agents. Ask why a service is erroring; the agent reads traces, logs and code to answer. |
![]() Errors. Exceptions grouped into issues you can assign, resolve and alert on. |
![]() Session replay. Watch the browser session behind a slow request or a frontend error. |
Quickstart
Send telemetry to maple.dev. Create an ingest key in Settings, then configure any OpenTelemetry SDK or collector:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.maple.dev"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_INGEST_KEY"
Or try it on your machine. One binary with OTLP ingest on :4318, an embedded ClickHouse and the dashboard:
brew install Makisuo/tap/maple
maple start
No Homebrew? curl -fsSL https://maple.dev/cli/install | sh. See docs/local-mode.md for details.
Connect your coding agent to the MCP server:
claude mcp add --transport http maple https://api.maple.dev/mcp
Repository layout
| Path | What lives there |
| --------------------------- | ------------------------------------------------------------------------ |
| apps/web | Dashboard (Foldkit, Vite) |
| apps/api | Effect HTTP API, auth and OAuth |
| apps/ai | MCP server, chat agent and investigations |
| apps/ingest | OTLP ingest gateway: key auth, org enrichment, forwarding |
| apps/alerting | Alert evaluation worker |
| apps/cli, apps/local-ui | The maple CLI and the local-mode dashboard |
| apps/landing | maple.dev and the docs |
| apps/ios | Native SwiftUI app |
| packages/* | Shared Maple code: domain, query-engine, backend, db, ui, SDKs |
| lib/* | Standalone libraries with no Maple knowledge |
Development
Prerequisites
- Bun
>=1.3
Install
bun install
Develop
Run the whole stack — the Cloudflare Workers under alchemy's local runtime,
the rest as child processes of the same alchemy dev — behind
https://:
bun dev
Or just some of it (api, alerting, electric-sync, web, landing,
ingest, local-ui, scraper):
bun dev api web
A single non-Worker app can also run on its raw port, outside the stack:
bun --filter=@maple/web dev
Validate
bun run typecheck
bun run build
bun run test
Docker (Local)
Run the local multi-service stack (API + web + ingest + otel collector):
docker compose -f docker-compose.yml up --build
Services:
- API:
http://localhost:3472 - Web:
http://localhost:3471 - Ingest:
http://localhost:3474 - OTEL collector:
4317(gRPC),4318(HTTP),13133(health/extensions)
Cloudflare Deploy (Alchemy)
Deployments run on Alchemy v2 (Effect-based): the root alchemy.run.ts exports a
single Alchemy.Stack("maple", …) whose program yields one module per app:
apps/api/src/worker.ts— the api Worker: Hyperdrive (PlanetScale Postgres)MAPLE_DB,
ChatSession Durable Object, all yielded from its init
apps/alerting/src/worker.ts— cron-driven alerting Worker (cross-script workflow ref)apps/electric-sync/src/worker.ts— ElectricSQL shape-proxy Workerapps/web/src/worker.ts/apps/landing/src/worker.ts/apps/local-ui/src/worker.ts
Command.Build + asset-serving Workers
Stage grammar is prd / pr- / dev names, resolved via
@maple/infra/cloudflare (parseMapleStage, resolveMapleDomains, resolveWorkerName,
resolveHyperdriveRefId, resolveMapleProfile). prd binds the
dashboard-managed Hyperdrive by config ID (resolveHyperdriveRefId) — origin credentials
never touch a deploy. MAPLE_PG_URL is only needed for dev stages, whose Hyperdrive alchemy
manages itself. PR previews bind no database at all (their profile's database is "none"):
DB-backed routes 500, everything else in the preview works.
Run locally:
PR_NUMBER=123 bun run alchemy:deploy:pr
The first v2 deploy against a stage with live v1-created resources needs --adopt
(the pr script passes it already); v1 state is incompatible and simply abandoned —
never run a v1 alchemy destroy against a live stage.
Tear down:
PR_NUMBER=123 bun run alchemy:destroy:pr
CI workflows:
- PRD (manual only via
workflow_dispatch):.github/workflows/deploy-prd.yml - PR preview lifecycle:
.github/workflows/deploy-pr-preview.yml(pull_requestopened/synchronize/reopened/closed)
- Secrets are fetched from Infisical via
Infisical/secrets-actionusing OIDC
INFISICAL_PROJECT_SLUG (the project slug — a
variable, not a secret: GitHub masks secret values everywhere, and a
slug like maple would then blank out the PR-preview deployment URL
app-pr-.maple.dev )
- GitHub environment secret INFISICAL_MACHINE_IDENTITY_ID in each of production,
production-eu, pr-preview and maintenance, each a different identity (see below).
No repo-level copy: a job outside those environments gets no identity at all.
- Infisical environments (
prod,dev— mapped from the old Doppler
prd/pr configs) must define:
- CLOUDFLARE_API_TOKEN
- CLOUDFLARE_DEFAULT_ACCOUNT_ID (bridged to alchemy v2's CLOUDFLARE_ACCOUNT_ID in the root alchemy.run.ts; ALCHEMY_PASSWORD/ALCHEMY_STATE_TOKEN were v1-only and are no longer read)
- TINYBIRD_HOST
- TINYBIRD_TOKEN
- EMAIL_FROM (sender address on an onboarded Cloudflare Email Service domain; delivery uses the EMAIL worker binding, no API key)
- MAPLE_INGEST_KEY_ENCRYPTION_KEY
- MAPLE_INGEST_KEY_LOOKUP_HMAC_KEY
- MAPLE_AUTH_MODE
- MAPLE_ROOT_PASSWORD (required in self_hosted mode)
- CLERK_SECRET_KEY
- CLERK_PUBLISHABLE_KEY
- CLERK_JWT_KEY
Setup note: one Infisical machine identity per GitHub environment, each with GitHub OIDC auth
bound to the exact subject repo:MapleTechLabs/maple:environment: (no globs: Infisical's
* also matches : and /) and a custom project role that reads only its Infisical
environment: production → prod, production-eu → prod-eu, pr-preview and
maintenance → dev (or a dedicated preview environment via PREVIEW_INFISICAL_ENV_SLUG).
production, production-eu and maintenance only deploy from main. The workflows select
secrets via project-slug (INFISICAL_PROJECT_SLUG) and per-stage env-slug.
Runtime API URL behavior:
- Deploy-time web builds resolve
VITE_API_BASE_URLfrom the Cloudflare api worker domain (api.maple.devinprd, worker.dev URL forpr-*). - Local
bun --filter=@maple/web devcan still use root.envVITE_API_BASE_URLfor local API routing.
Environment
- Canonical env example:
.env.example - API-only env example:
apps/api/.env.example - Real
.envvalues are local-only and should stay untracked.
VITE_API_BASE_URL to point to the API (defaults to http://localhost:3472).
For ingest + key auth, set these at minimum in your root .env when running the ingest gateway:
MAPLE_INGEST_KEY_LOOKUP_HMAC_KEYMAPLE_INGEST_KEY_ENCRYPTION_KEY(required when ingest reads encrypted credentials from Postgres)INGEST_PORTINGEST_FORWARD_OTLP_ENDPOINTMAPLE_INTERNAL_ORG_ID(the org the gateway's own telemetry is filed under; no default)INGEST_FORWARD_TIMEOUT_MSINGEST_MAX_REQUEST_BODY_BYTESINGEST_REQUIRE_TLSINGEST_REPLAY_MAX_SESSION_BYTES(optional; ceiling on the decompressed rrweb
0 disables)
Persistence (PostgreSQL / PGlite)
Maple persists application state in PostgreSQL:
- Production uses PlanetScale Postgres through Cloudflare Hyperdrive.
- Wrangler development uses the Docker Postgres started by
bun db:up. - Non-Worker local entrypoints use embedded PGlite under
apps/api/.data/pglite; set
MAPLE_DB_URL=memory:// for an ephemeral database.
Migration commands:
bun db:up
bun db:migrate:local
bun run --cwd packages/db db:generate
bun run --cwd packages/db db:studio
CI migrates deployed PlanetScale branches over their direct port before Alchemy deploys the
Workers. See docs/persistence.md for the full workflow.
Ingest Keys
- Maple now manages per-org ingest keys in the database (
public+private). - Keys are available in Settings and can be rerolled independently.
- Reroll revokes the previous key immediately.
- Private ingest keys are encrypted at rest with
MAPLE_INGEST_KEY_ENCRYPTION_KEY(base64-encoded 32-byte key). - Ingest key lookup/auth uses non-reversible HMAC hashes via
MAPLE_INGEST_KEY_LOOKUP_HMAC_KEY.
Auth Modes
Maple supports exactly two auth modes via MAPLE_AUTH_MODE:
clerk
MAPLE_AUTH_MODE=clerk
- Set CLERK_SECRET_KEY
- Optionally set CLERK_JWT_KEY for networkless verification
- Set CLERK_PUBLISHABLE_KEY for the web app
- Optionally override VITE_CLERK_SIGN_IN_URL and VITE_CLERK_SIGN_UP_URL
self_hosted
MAPLE_AUTH_MODE=self_hosted
- Set MAPLE_ROOT_PASSWORD (required)
- Set MAPLE_DEFAULT_ORG_ID (defaults to default)
- Users must sign in at /sign-in with the root password before accessing the dashboard/API.
- Session tokens are valid for 12 hours and renew silently against
POST /api/auth/session/refresh, up to an absolute cap of 7 days from sign-in. Past the cap
the root password is required again. Rotating MAPLE_ROOT_PASSWORD still invalidates every
token immediately — it is the HMAC key, and the only way to revoke a session early.
Start apps:
bun --filter=@maple/api dev
bun --filter=@maple/web dev
Validate behavior:
- Clerk mode:
/sign-in
- Signed-in users without an active org are redirected to /org-required
- Signed-in users with an active org can query the API with bearer auth
- Self-hosted mode:
/sign-in
- MAPLE_ROOT_PASSWORD login issues a bearer session token with a bounded lifetime
- Protected API routes reject requests without a valid bearer session token
- Expired tokens are rejected; a still-valid one can be traded for a fresh one at
POST /api/auth/session/refresh until the session's absolute deadline
Breaking change:
- Self-hosted multi-tenant JWT/API-key auth paths were removed.
MAPLE_ROOT_PASSWORDis now required whenMAPLE_AUTH_MODE=self_hosted.





