@odeva/cli
v0.0.12
Published
Build apps on the Odeva booking platform — scaffold, develop, deploy.
Readme
@odeva/cli
Build apps on the Odeva booking platform — scaffold, develop, deploy.
The odeva CLI is the developer toolkit for the Odeva booking platform. It is to Odeva what shopify is to Shopify and wrangler is to Cloudflare: scaffold a project, run it locally with a public tunnel and live webhook delivery, then ship it.
Install
npm install -g @odeva/cli
# or
bun add -g @odeva/cliYou'll also need cloudflared on your PATH for the dev tunnel:
# macOS
brew install cloudflared
# Debian/Ubuntu
sudo apt install cloudflared
# Other platforms: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/Quickstart
odeva auth login # paste a personal access token
odeva app init my-app # scaffold a new app (default template: hono-bun)
cd my-app
bun install
odeva app config link # register the app on Odeva, write client_id to toml
odeva app dev # local server + tunnel + webhook auto-registrationIn another terminal:
odeva webhook trigger reservation.createdYou should see the fixture payload arrive in your dev server's logs, signed and ready to handle.
How odeva app dev works
┌─────────────────────────────────────────────┐
│ Odeva platform │
│ (creates webhook subscriptions, signs │
│ payloads, delivers events) │
└────────────────────┬────────────────────────┘
│ POST https://<id>.trycloudflare.com/webhooks/...
▼
┌──────────────────────────────────┐
│ cloudflared quick tunnel │
│ (started by `odeva app dev`) │
└────────────────┬─────────────────┘
│ http://localhost:3000
▼
┌──────────────────────────────────┐
│ Your Hono/Bun/Express app │
│ Verify signature with the │
│ ODEVA_WEBHOOK_SECRET injected │
│ into the dev process. │
└──────────────────────────────────┘On Ctrl-C, the CLI tears down the tunnel and deletes the webhook subscriptions it created, so your production endpoints aren't polluted with dev URLs.
Commands
| Command | What it does |
| --- | --- |
| odeva auth login | Authenticate the CLI with a personal access token |
| odeva auth logout | Remove stored credentials from this machine |
| odeva auth whoami | Show the currently authenticated session |
| odeva app init [name] | Scaffold a new app from a template (default: hono-bun) |
| odeva app config link | Register the local app on Odeva, write client_id back to toml |
| odeva app dev | Run the app locally with a public tunnel + webhook auto-registration |
| odeva webhook list | List active subscriptions on the current org (--available shows event types) |
| odeva webhook trigger <event> | Fire a signed sample payload at your local handler |
Run odeva help <command> for full flags on any command.
Config: odeva.app.toml
Each Odeva app has an odeva.app.toml at its root. Example:
name = "Cabin Manager"
slug = "cabin-manager"
client_id = "app_..." # written by `odeva app config link`
description = "A booking add-on for Foo Holiday Park."
[build]
dev = "bun run dev"
port = 3000
[access_scopes]
scopes = ["reservations:read", "reservations:write"]
# Omit this section if the app doesn't surface inside the merchant admin.
[admin]
entry_url = "https://app.example.com/admin"
[admin.sidebar]
label = "Cabin Manager"
icon = "puzzle"
[webhooks]
api_version = "2026-01"
[[webhooks.subscriptions]]
topic = "reservation.created"
uri = "/webhooks/reservation.created"
[[webhooks.subscriptions]]
topic = "payment.succeeded"
uri = "/webhooks/payment.succeeded"This file is the source of truth for app config. odeva app config link pushes it to the platform; odeva app dev reads webhook subscriptions from it to wire up the tunnel.
Embedding apps in the merchant admin
The admin embed surface lets your app appear as a sidebar entry inside the Odeva merchant admin, mounting your UI in an iframe. It is opt-in: the feature activates only when you declare an [admin] block in odeva.app.toml (see the Config example above) and run odeva app config link.
How it works
- Developer declares
[admin]inodeva.app.toml(see the Config example above) and runsodeva app config link. - Merchant installs the app from the marketplace.
- Merchant clicks the sidebar entry.
odeva-adminmints a 5-min EdDSA session token via themintAppSessionTokenGraphQL mutation, then loads the iframe atentry_url?session_token=<jwt>&host=<base_url>. - The app verifies the token against the platform JWKS at
/.well-known/odeva/apps/jwks.json, checkingissandaud. - Before the token expires, the app's backend calls
POST /api/apps/session-token/refreshwithX-Api-Key: <api_key>and hands the new token to the iframe.
Session token claims
Tokens are standard JWTs signed with EdDSA; the JOSE header carries alg, typ, and kid.
| Claim | Value |
| --- | --- |
| iss | Platform base URL (matches your ODEVA_API_URL) |
| aud | The app's client_id (matches your ODEVA_APP_CLIENT_ID) |
| sub | The app installation ID (string) |
| org_id | The merchant organization ID (string) |
| app_id | The app ID (string) |
| exp | 5 minutes after iat |
| iat | Issued-at (unix seconds) |
| jti | Unique token nonce |
Verifying the token
The JWKS endpoint is ${ODEVA_API_URL}/.well-known/odeva/apps/jwks.json. The scaffolded template (odeva app init) provides a reference implementation in src/admin.ts; it uses the jose library with createRemoteJWKSet and jwtVerify.
Key points when verifying:
- Check
issmatches yourODEVA_API_URL. - Check
audmatches yourODEVA_APP_CLIENT_ID. - Never accept
alg: "none"—jose'screateRemoteJWKSetenforces this automatically; if you roll your own verifier you must enforce it yourself.
Refreshing the token
Tokens have a 5-minute TTL. Long-lived iframe sessions must refresh the token before it expires.
Refresh from the app's backend only — the api_key must not be exposed to the browser.
curl -X POST "$ODEVA_API_URL/api/apps/session-token/refresh" \
-H "X-Api-Key: $ODEVA_API_KEY"
# -> { "token": "...", "expires_at": "2026-..." }Success response: { token, expires_at } where expires_at is an ISO8601 timestamp.
Error codes:
| Status | Meaning |
| --- | --- |
| 401 | Missing, invalid, or revoked api_key |
| 403 | api_key is not an app-installation key (e.g. a merchant key) |
| 409 | Installation is not active, or the app has no [admin] declared |
| 503 | Platform signing key not configured (operator-side; retry) |
Typical pattern: the backend refreshes ~30 s before expires_at and pushes the new token to the iframe via your own channel (postMessage, polling, or whatever fits the app).
Quick reference
- JWKS:
${ODEVA_API_URL}/.well-known/odeva/apps/jwks.json - Refresh:
${ODEVA_API_URL}/api/apps/session-token/refresh - Reference implementation:
src/admin.ts(scaffolded byodeva app init) - Env vars:
ODEVA_API_URL,ODEVA_APP_CLIENT_ID,ODEVA_API_KEY - Not yet shipped: host↔app
postMessagebridge, URL sync between iframe and host.
Templates
Bundled templates live under templates/ in this repo:
hono-bun(default) — Hono on Bun, runtime-agnostic. Deploys later to Node, Bun, Cloudflare Workers, or Deno without rewrites.
More templates (Next.js, Express, Fresh, WordPress) are on the roadmap — drop a directory into templates/ and it's available via odeva app init --template <name>.
Authentication
For now, the CLI authenticates with a personal access token (PAT). Generate one from the Odeva admin panel and paste it into odeva auth login. The token is stored at ~/.config/odeva/credentials.json with mode 0600.
Browser-based OAuth (device-code flow) lands once the developer dashboard ships.
Status
Pre-1.0. The command surface and odeva.app.toml schema are stabilizing. Expect breaking changes between minor versions until 1.0.
Development
git clone https://codeberg.org/odeva/odeva-cli
cd odeva-cli
bun install
bun run test
bun run typecheck
bun run build
node ./bin/run.js --helpLicense
MIT — see LICENSE.
