Maple is now organized as a monorepo with a SPA frontend and an Effect-based backend API.
apps/web: TanStack Router SPA (Vite)apps/api: Effect HTTP API (Tinybird proxy + MCP server code)apps/ingest: OTLP ingest gateway (key auth + org enrichment + collector forwarding)apps/landing: Astro landing sitepackages/domain: Shared Effect HTTP contracts and domain typespackages/api-client: Typed client used by the web app
- Bun
>=1.3
bun installRun all app services (API + web + ingest + landing):
bun run dev:alldev:all uses Turbo --continue=always, so if one service fails to bind (for example, a port already in use), other services keep running.
Dev scripts run Turbo in TUI mode so interactive dev servers (like Vite) stay attached.
Use strict fail-fast mode when needed:
bun run dev:all:strictRun every available dev task in the monorepo:
bun run devRun only web:
bun run dev:webRun only API:
bun run dev:apiRun only ingest gateway:
bun run dev:ingestbun run typecheck
bun run build
bun run testRun the local multi-service stack (API + web + ingest + otel collector):
docker compose -f docker-compose.yml up --buildServices:
- API:
http://localhost:3472 - Web:
http://localhost:3471 - Ingest:
http://localhost:3474 - OTEL collector:
4317(gRPC),4318(HTTP),13133(health/extensions)
Deployments are orchestrated from one Alchemy run in apps/web/alchemy.run.ts with stage-driven targets:
- Provisions Railway project
maple - Uses Railway environments per stage:
prd,stg, andpr-<number> - Provisions stage-scoped services
api,ingest, andotel-collector - Configures service instance settings (
rootDirectory,dockerfilePath,watchPatterns) - Applies required Railway variables for API and ingest
- Uses production custom Railway domains (
api.maple.dev,ingest.maple.dev) and Railway-generated domains instg/pr-* - Deploys
apps/webto Cloudflare
Railway orchestration module: @maple/infra/railway (workspace package at packages/infra).
Run locally:
bun run deploy:prd
bun run deploy:stg
PR_NUMBER=123 bun run deploy:prAll deploy scripts are full-stack only (Railway + Cloudflare). If RAILWAY_API_TOKEN is missing, deploy/destroy fails immediately.
Tear down:
bun run destroy:prd
bun run destroy:stg
PR_NUMBER=123 bun run destroy:prCI workflows:
- PRD:
.github/workflows/deploy-prd.yml(pushonmain+workflow_dispatch) - STG:
.github/workflows/deploy-stg.yml(pushondevelop+workflow_dispatch) - PR preview lifecycle:
.github/workflows/deploy-pr-preview.yml(pull_requestopened/synchronize/reopened/closed)
Secrets source model (CI):
- GitHub Secrets (only one):
DOPPLER_TOKEN - Doppler configs (
prd,stg,pr) must define:ALCHEMY_PASSWORDALCHEMY_STATE_TOKENCLOUDFLARE_API_TOKENCLOUDFLARE_ACCOUNT_IDCLOUDFLARE_EMAILRAILWAY_API_TOKENRAILWAY_WORKSPACE_IDTINYBIRD_HOSTTINYBIRD_TOKENMAPLE_DB_URLMAPLE_DB_AUTH_TOKENMAPLE_INGEST_KEY_ENCRYPTION_KEYMAPLE_INGEST_KEY_LOOKUP_HMAC_KEYMAPLE_AUTH_MODEMAPLE_ROOT_PASSWORD(required inself_hostedmode)CLERK_SECRET_KEYCLERK_PUBLISHABLE_KEYCLERK_JWT_KEY
Free/Starter note: when using a personal Doppler token, the workflow must also specify Doppler selectors (doppler-project, doppler-config). This repo uses maple with stage configs prd, stg, and pr.
Runtime API URL behavior:
- Deploy-time web builds always use the Railway API domain from the same Alchemy run (
api.maple.devinprd, Railway-generated instg/pr-*). - Local
bun run dev:webcan still use root.envVITE_API_BASE_URLfor local API routing.
Legacy env cleanup (delete these from Doppler):
MAPLE_DEPLOY_WEB_ONLY
- Canonical env example (used by
bun run dev:all):.env.example - API-only env example:
apps/api/.env.example - Real
.envvalues are local-only and should stay untracked.
The web app expects 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 using bun run dev:all:
MAPLE_INGEST_KEY_LOOKUP_HMAC_KEYINGEST_PORTINGEST_FORWARD_OTLP_ENDPOINTINGEST_FORWARD_TIMEOUT_MSINGEST_MAX_REQUEST_BODY_BYTESINGEST_REQUIRE_TLS
Maple now persists dashboards in SQLite via libSQL:
- Default local mode: no Turso CLI needed. If
MAPLE_DB_URLis unset, Maple usesapps/api/.data/maple.db. - Turso cloud mode: set
MAPLE_DB_URLto your Turso/libSQL URL andMAPLE_DB_AUTH_TOKENto your token. - Self-hosting: persist the
apps/api/.datadirectory as a volume so dashboard state survives container/restart cycles.
Migration commands:
bun run db:migrate
bun run db:generate
bun run db:push
bun run db:studioWhen running the API (bun run dev:api or bun run --filter=@maple/api start), migrations are applied automatically before boot.
- 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.
Maple supports exactly two auth modes via MAPLE_AUTH_MODE:
clerk- Create a Clerk application with Organizations enabled.
- Set
MAPLE_AUTH_MODE=clerk - Set
CLERK_SECRET_KEY - Optionally set
CLERK_JWT_KEYfor networkless verification - Set
CLERK_PUBLISHABLE_KEYfor the web app - Optionally override
VITE_CLERK_SIGN_IN_URLandVITE_CLERK_SIGN_UP_URL
self_hosted- Set
MAPLE_AUTH_MODE=self_hosted - Set
MAPLE_ROOT_PASSWORD(required) - Set
MAPLE_DEFAULT_ORG_ID(defaults todefault) - Users must sign in at
/sign-inwith the root password before accessing the dashboard/API.
- Set
Start apps:
bun run dev:api
bun run dev:webValidate behavior:
- Clerk mode:
- Signed-out users are redirected to
/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
- Signed-out users are redirected to
- Self-hosted mode:
- Signed-out users are redirected to
/sign-in MAPLE_ROOT_PASSWORDlogin issues a bearer session token- Protected API routes reject requests without a valid bearer session token
- Signed-out users are redirected to
Breaking change:
- Self-hosted multi-tenant JWT/API-key auth paths were removed.
MAPLE_ROOT_PASSWORDis now required whenMAPLE_AUTH_MODE=self_hosted.