Interfaces
Pando's external interfaces — everything that goes in and everything that comes out. Start here when you need to know what a surface accepts rather than how it is built.
R-261 makes one of these authoritative: the HTTP API is the product. The console, the CLI and the MCP server are clients of it, and none has a capability the API lacks. When two documents here disagree, the API reference is the one that is right.
The first three are generated from the code that serves them by make reference, and CI fails
when they are out of date. They describe the binary rather than the intent — when one of them
disagrees with a design document, the design document is the one that is behind.
| Interface | Reference |
|---|---|
| HTTP API — every endpoint, verb and error code | api.md (generated) |
| CLI — every command and flag | cli.md (generated) |
| MCP — every tool and its arguments | mcp.md (generated) |
| HTTP API, as designed | design/04-api.md |
| App spec — the object almost everything operates on | design/01-spec-schema.md |
| Adapter interfaces — for contributing one | design/03-adapter-interfaces.md |
| Authorization verbs and the proxy's request contract | design/06-authorization-and-proxy.md |
HTTP API#
Base path /api/v1. JSON in and out. Every response carries an X-Request-Id.
Authentication, in precedence order:
Authorization: Bearer tok_…— a token principal, frompando token.Cookie: pando_session=ses_…— a browser session, fromPOST /api/v1/sessions.
Resources, in full, are in design/04-api.md §2: apps, deploys, specs, slots,
secrets, environment, volumes, grants, users, groups, roles, host policy, backups, tokens and the
audit log.
Errors are one envelope everywhere. The code is stable and machine-readable; the message and
remedy are written to be pasted into an assistant and acted on without further context (R-105).
{
"code": "PLAN_SLOT_UNFILLED",
"message": "This app needs a PostgreSQL database, and one hasn't been chosen yet.",
"remedy": "Choose how to fill the database slot: provision one inside this app, connect to an existing one, or paste a connection string.",
"details": { "slots": [{ "key": "database", "type": "postgres" }] },
"request_id": "req_01HQ8…"
}Code prefixes and what they mean: AUTH_* the caller is not who they need to be, PERM_* they are
but may not do this, POLICY_* host policy forbids it for everyone, PLAN_* the app cannot be
deployed as configured, VALID_* the request is malformed, STATE_* the object is in the wrong
state for this, CAPACITY_* a resource is exhausted, BACKUP_* and ADAPTER_* name their subsystem.
A WARN_* is never a blocker — anything that blocks is a PLAN_* error.
IDs are prefixed, sortable and opaque: app_, spec_, usr_, tok_, vol_, gr_, ses_,
req_, with a ULID body. Treat them as strings; the prefix is for reading, not parsing.
Time is RFC 3339, UTC, everywhere on the wire.
Pagination is cursor-based: ?limit=50&cursor=…, and the response carries next_cursor.
Idempotency: POST endpoints that create infrastructure accept an Idempotency-Key header, and
a retry replays rather than repeats.
What an app receives#
An app deployed on Pando is handed three things, and the distinction between the first and the second is the whole security model.
X-Pando-Assertion — a signed Ed25519 JWT describing the caller. This is the only statement about
identity an app should trust. Verify it against the JWKS Pando publishes at /.well-known/jwks.json.
Claims: sub (stable per user, independent of email and of the identity adapter), email, name,
groups, aud (the app's ID — this is what stops an assertion minted for one app being replayed
against another), iat, exp, iss. Valid for 120 seconds.
Other X-Pando-* headers — the same information, unsigned, for convenience. Explicitly not
trustworthy on their own (R-053). Inbound X-Pando-* headers from a client are stripped
unconditionally before a request reaches an app, so they cannot be forged; but an app that reads them
instead of verifying the assertion is trusting Pando's proxy rather than a signature, and there is no
way for it to tell the difference if the proxy is ever bypassed.
Environment variables — the app's own configuration, plus anything injected by a filled slot (database credentials and connection strings) and any secret bound to it. Nothing is read from the repository at deploy time (R-020): the spec is the sole record of how an app runs.
An app never receives a pando_* cookie. Those are stripped on the way out (R-173).
Configuration#
Set as environment variables, or in a config file. The environment prefix is PANDO_, and a nested
setting is joined with an underscore: server.base_domain is PANDO_SERVER_BASE_DOMAIN.
| Variable | Default | Purpose |
|---|---|---|
PANDO_DATABASE_URL |
the bundled Postgres | Point Pando at an existing database. |
PANDO_SERVER_ADDR |
:8080 |
Address the console and API listen on. |
PANDO_SERVER_BASE_DOMAIN |
localtest.me |
Domain per-app subdomains are taken from, under hostname routing. |
PANDO_SERVER_ROUTING_MODE |
port | How apps are addressed: port, subdomain or path. |
PANDO_SERVER_ISSUER |
derived | The iss claim in identity assertions. |
PANDO_SERVER_EXTERNAL_URL |
— | The address browsers reach this installation on, such as https://pando.example.com. Set it when something other than Pando terminates TLS: it is what marks the session cookie Secure. Unset means "use the request", which is right on a localhost install and when Pando serves TLS itself. |
PANDO_SERVER_PROXY_UPSTREAM |
— | Where the proxy sends traffic it has authorized. |
PANDO_SERVER_WORK_DIR |
/var/lib/pando |
Build contexts, uploads and adapter state. |
PANDO_APP_PORT_START / _END |
9000 / 9019 |
Range of host ports apps are allocated. |
PANDO_ADMIN_PASSWORD |
generated | Initial admin password. Read on first run only. |
PANDO_LOG_LEVEL |
info |
Log verbosity. |
PANDO_RECONCILER_BACKOFF |
see R-149 | Retry schedule. Compressing it is for tests; pando warns when it is set faster than the shipped default. |
PANDO_PORT is not read by Pando. It is a variable in the shipped docker-compose.yml, which uses
it to choose the host port published in front of the container's fixed 8080.
A config file is read only when one is named with pando serve --config <path>. The environment
wins over the file.
Host policy at startup#
Any host policy setting can also be fixed at startup, in a policy: section of the config file or
as PANDO_POLICY_<SETTING>:
policy:
min_security_score: 70
disabled_verbs: [app.exec]
public_sharing: passcode_only # allowed, passcode_only or nonePANDO_POLICY_MIN_SECURITY_SCORE=70
PANDO_POLICY_DISABLED_VERBS=app.exec,app.secrets.read # lists are comma-separated
PANDO_POLICY_PUBLIC_SHARING=passcode_onlyA setting fixed this way overrides what is saved in the console, applies everywhere policy is
checked, and cannot be changed from the console, the API or the CLI while it is set: the console
shows it disabled, with where it is set, and PUT /api/v1/policy refuses a change to it. It is
never written into the saved policy, so removing it and restarting brings back what was saved. A
name that is not a policy setting, or a value that does not read as one, stops Pando at startup
rather than being ignored.
The settings are source_allowlist, disabled_verbs, agent_disabled_verbs,
public_sharing, allow_anonymous_grants, min_build_isolation, min_runtime_isolation, egress_allowlist,
require_backup_before_destroy, max_token_lifetime_days, max_log_disk_bytes,
disable_ai_screening, min_security_score, insecure_action, insecure_grace_hours and
ignore_unfixable_findings.
public_sharing is how an app may be shared with everyone: allowed (with or without a passcode),
passcode_only, or none. The older allow_anonymous_grants: false still means none when
public_sharing is unset.
GET /api/v1/config, pando config and the Policy screen list every setting Pando started with,
its value, and where it came from. Secrets are never shown.
Guarantees worth relying on#
These are requirements, not implementation details, and they will not be changed without a major version:
- A user's
subis stable across email changes and across a change of identity adapter (R-054). An app may key its own data on it. - The audit log is append-only, at the database grant level (R-027). Nothing that happened stops having happened.
- Spec revisions are append-only, enforced by a database trigger, so a rollback target cannot be rewritten under you. The last ten pinned specs are retained (R-152).
- Deleting an app decides what happens to its data rather than assuming. Interactively Pando asks
whether to keep a final backup (R-204); non-interactively, through the CLI, API or MCP, it backs up
by default and
--forceis what skips it (R-205). - A failed app stays failed until a person acts (R-151). Nothing retries it back into existence.
- Warnings never block. Anything that blocks is a
PLAN_*error, and that split is deliberate: a warning shaped like an error teaches people to ignore both.
Other documents#
../SECURITY.md— the security model, the cryptography in use, and how to report a vulnerability.releasing.md— version numbering, and how to verify a download's signature.../CONTRIBUTING.md— building it, and what a change needs before it merges.requirements.md— what Pando is, as 210 numbered requirements. The authority behind everything above.traceability/requirements-index.md— generated: which requirement is implemented where, and which have acceptance tests.