Platform
peppy platform connects this machine to the Peppy platform. Signing in
gives the CLI a session for the platform API. Enrolling joins this machine
to a project’s cloud messaging router: the CLI mints a private key and a
certificate signing request, the platform signs it, and the daemon dials the
project’s router with that certificate from then on. peppy platform login
does both, one after the other: it signs you in, then enrolls this machine in
the project you select, unless you decline. peppy platform enroll enrolls a
machine that already has a session.
The session is yours; the enrollment is the machine’s. Logging out does not unenroll, and the daemon stays connected with no session for as long as its certificate is valid. The daemon needs the session for one thing: to renew that certificate.
Peppy is a public OAuth client of the platform’s identity provider (Zitadel): the CLI obtains a bearer token through the browser and sends it to the backend, which validates it. The CLI never sees your Google/passkey credentials. Those stay in the browser.
Commands
Section titled “Commands”| Command | What it does |
|---|---|
peppy platform login [--api-url <url>] [--no-browser] [--workspace <id|name>] [--project <id|name>] [--no-enroll] [--yes] | Signs in via the OAuth 2.0 Device Authorization Grant, caches the tokens, then enrolls this machine in the project you select. |
peppy platform logout | Revokes the session’s tokens at the identity provider and deletes them locally, with the selection. The enrollment stays. |
peppy platform workspace show [--json] | Shows the selected workspace. |
peppy platform workspace list [--json] | Lists the workspaces you belong to, the selected one marked. |
peppy platform workspace use [<id|name>] | Selects the workspace the commands act on. |
peppy platform workspace clear | Removes the selected workspace and its project. |
peppy platform project show [--json] | Shows the selected project. |
peppy platform project list [--workspace <id|name>] [--json] | Lists the projects of a workspace, the selected one marked. |
peppy platform project use [<id|name>] [--workspace <id|name>] | Selects the project the commands act on. |
peppy platform project clear | Removes the selected project; its workspace stays selected. |
peppy platform whoami [--json] | Shows the signed-in identity, backend, and token validity. |
peppy platform enroll [--workspace <id|name>] [--project <id|name>] [--replace] [--yes] | Enrolls this machine as a peer of a project’s cloud router and restarts the daemon under the new identity. |
peppy platform unenroll [--local-only] [--yes] | Removes this machine’s peer from the platform, deletes the local enrollment, and restarts the daemon standalone. |
peppy platform status [--json] | Shows the enrollment, whether the daemon runs under it with a verified link, and what the platform reports about the router and this peer. |
peppy platform peers [--workspace <id|name>] [--project <id|name>] [--json] | Lists the peers of a project’s cloud router, this machine’s project by default. |
peppy platform router restart [--workspace <id|name>] [--project <id|name>] [--yes] | Restarts a project’s cloud router, which applies every pending change. |
peppy platform router start [--workspace <id|name>] [--project <id|name>] | Starts a stopped router. |
--api-url overrides the backend on any command. --yes (-y) skips the
confirmation prompt of the command: the daemon restart described under
Enrolling, or the router restart described under
The project router.
login and enroll need the peppy daemon to run on this machine: the machine
enrolls under the name of its core node, and the daemon is what joins the
project router. Start it with peppy service serve, or install it as a
background service with peppy service install.
Signing in
Section titled “Signing in”peppy platform loginOn a terminal this prints a verification URL and a user code, then opens your
browser at that URL (with the code pre-filled). Approve the request in the
browser and the CLI stores the tokens. Subsequent commands reuse them: you sign
in once and stay signed in until the refresh token expires; expired access
tokens are refreshed automatically. Then login enrolls this machine; see
Enrolling at sign-in.
Over SSH or anywhere without a browser, use --no-browser: the CLI prints the
URL and code and waits for you to approve them on another device.
peppy platform login --no-browserHow it works
Section titled “How it works”- The CLI fetches the public
GET {api_url}/cli/auth-config, which returns theissuer, theclient_id, and the exactscopesto request. - It runs OIDC discovery against the
issuer({issuer}/.well-known/openid-configuration) to find the device, token and revocation endpoints. - It starts the device flow, opens the browser, and polls until you approve.
- It caches the tokens (and the
issuer/client_id, so refresh works offline) under~/.peppy/conf/credentials.json5.
Every later call to the platform API sends the cached access token as a bearer.
When it is about to expire, or the backend answers 401, the CLI exchanges the
refresh token for a new one and persists the rotation.
Enrolling at sign-in
Section titled “Enrolling at sign-in”After the sign-in, login enrolls this machine in a project, as
enroll does:
--workspaceand--projectname the project, by id or by exact name.- With no flag, the only workspace, and the only project of the workspace, are selected with no question. Archived projects do not count. An account with one project is enrolled in it at once.
- With more than one, a menu asks for the workspace, then for the project.
Move with the arrow keys (or
kandj) and select with Enter. The last entry of each menu, Do not enroll this machine (sign in only), declines the enrollment, and so do Esc,qand Ctrl-C.
Logged in as [email protected] (dev)✔ Workspace to enroll this machine in: Robotics lab (team)? Project of Robotics lab to enroll this machine in ↑/↓ to move, enter to select, esc to cancel Lab❯ Field Do not enroll this machine (sign in only)The selected workspace and project become the selection, and the enrollment follows:
✔ Project of Robotics lab to enroll this machine in: FieldEnrolling cn-faithful-hare in project Field (ef2288ee-81d8-402e-922b-dbe4a4edbf25) in workspace Robotics lab.The platform signed the peer certificate. It expires on 2027-01-01, and the daemon renews it from 2026-12-02, while this machine has a session.Router link verified (tls/rtr-p.us-east-1.robocloud.dev.peppy.bot:7447).--no-enroll signs in only. A machine that is enrolled already keeps its
enrollment, and login prints the project it is enrolled in; peppy platform enroll --replace moves it.
login succeeds when the machine is enrolled, or when you declined the
enrollment. Every other way the enrollment does not happen fails the command,
with a message that says the session is kept and the command to run next:
- No daemon runs.
loginstops before the device flow starts, so you never approve a code for nothing. Start the daemon, or pass--no-enroll. --workspaceor--projecton an enrolled machine.loginstops before the device flow starts: it does not move an enrolled machine.- More than one choice and no terminal to show a menu on.
loginlists the choices and namespeppy platform enrollwith the flag to pass. - The platform refuses the enrollment. See Enrolling.
A stored selection of a different identity or backend is removed at sign-in: its ids mean nothing to the new account.
The selected workspace and project
Section titled “The selected workspace and project”The selection is the workspace, and the project of that workspace, that you
selected. It is the default target of the platform commands, so you name a
project one time and not on each command. It lives in
~/.peppy/conf/platform_selection.json5 and belongs to one backend and one
signed-in identity. login selects the project it enrolls this machine in.
Selecting
Section titled “Selecting”peppy platform workspace use # select from a menupeppy platform workspace use "Robotics lab"peppy platform project use # select from a menupeppy platform project use Fieldpeppy platform project use Arm --workspace "Robotics lab"Each use command picks one candidate:
- The argument names it, by id or by exact name.
- With no argument, the only candidate is selected with no question. Archived projects do not count.
- With more than one, a menu asks on a terminal. It starts on the selected candidate, and Esc cancels it.
- With more than one and no terminal, the command stops, lists the choices, and names the argument to pass.
project use looks for the project in the workspace that --workspace names,
else in the selected workspace, else in the only one; with none of them it
asks for the workspace first. workspace use keeps the selected project when
it selects the workspace of that project again. A different workspace starts
with no project: run project use next.
Showing and clearing
Section titled “Showing and clearing”peppy platform workspace listpeppy platform project listpeppy platform workspace showpeppy platform project showpeppy platform project clearpeppy platform workspace clearworkspace list lists your workspaces, and project list the projects of the
selected workspace (or of --workspace); a * marks the selected ones.
project clear removes the selected project and keeps its workspace;
workspace clear removes both.
Which project a command uses
Section titled “Which project a command uses”| Command | Order |
|---|---|
login | The flags, then the only choice, then the menu. |
enroll, project list | The flags, then the selection, then the only choice. |
peers, router restart, router start | The flags, then the selected project, then the project this machine is enrolled in. |
status, unenroll | The project this machine is enrolled in. |
Selection and enrollment
Section titled “Selection and enrollment”The selection is for the CLI only. A new selection does not move this machine
to a different project and does not restart the daemon. When this machine is
enrolled in a different project than the selected one, project use, project show and status say so. To move the machine, run peppy platform enroll --replace.
A selection that is out of date
Section titled “A selection that is out of date”The selection holds the ids and the names from the time you made it. A project
can be archived, or you can lose access to it, after that. The platform then
refuses the call, and the command adds that the selected project can be out of
date. Run peppy platform project use to select again.
Enrolling
Section titled “Enrolling”peppy platform enrollEnrollment joins this machine to one project’s cloud router: the
selected project, or the one that
--workspace and --project name (an id or the exact name). login runs the
same enrollment after a sign-in.
The daemon must run. The peer takes the name of the daemon’s core node, so the
platform and the other machines of the project know this machine by the name
peppy stack list shows. Set that name with
core_node_name
in ~/.peppy/conf/peppy_config.json5, or with peppy service serve --core-node-name <name>; when neither is set, the daemon derives a name that
stays the same for the machine.
With no daemon running, enroll stops before it calls the platform.
What happens:
- The CLI reads the core-node name of the running daemon, and resolves the workspace and project.
- It mints a P-256 key pair and a certificate signing request whose only subject is the peer name. The private key never leaves the machine.
- It posts the request to the project’s
router/peersendpoint. The platform signs it and answers with the certificate, the project CA, the address of the project router, the routing namespace (the project id), and the zenoh identity the daemon’s router must run under. - It writes the enrollment bundle under
~/.peppy/conf/peer/:enrollment.json5(the record),peer.key(the private key, owner-only),peer.crt(the signed certificate alone),ca.crt(the project CA),chain.crt(the chain above the certificate, foropenssl verify), andzenohd.platform.json5(the peer config the platform rendered, kept for inspection). The daemon presentspeer.crtand nothing above it, so the cloud router closes the link when that certificate expires. - Under
zenoh.managed, it pokes the running daemon over its control socket. The daemon’s router identity and session namespace cannot change while live, so the daemon restarts its generation under the new identity and the CLI waits for it to come back. A second poke then verifies the mutual-TLS link to the cloud router with a real handshake; a link that does not validate is reported with its reason, and the enrollment is kept.
The restart wipes the running node stack. When a daemon is running with user
nodes on a terminal, the command asks for confirmation first; --yes skips
the prompt, and a script (no TTY) is never prompted.
Under zenoh.external the daemon is left alone and the command prints where
the material is for the operator’s router to use.
When the platform refuses the enrollment, the command prints the platform’s reason. Two refusals come with a remedy:
- The router admits no more peers. A removed peer keeps its slot until the
router restarts. When the platform says how many slots a restart frees, the
command prints the
router restartcommand for a number above zero, and tells you to remove a peer first for zero. When the platform does not say, the command lists the peers of the router: each one that readspending_restartis a slot that a restart frees. - The platform could not sign the certificate in time. The command prints the delay the platform asks for. Run it again after that delay.
The local listener
Section titled “The local listener”The managed router listens on all interfaces in plain TCP, so that container nodes and the other machines of a LAN stack can reach it. On an enrolled machine this also means that a machine on the same network can open a session to this router and, through it, reach the project under this peer’s certificate. Where that is not acceptable, close the messaging port to other machines with the firewall of the host.
The certificate
Section titled “The certificate”The peer certificate is valid for 90 days, and the daemon renews it. The
renewal becomes due after two thirds of the lifetime of the certificate, which
is day 60 of 90. The lifetime is the one the certificate in peer.crt states,
so the schedule follows the certificate and not a clock of this machine. The
daemon then asks the platform for a new certificate for the same key and
writes it over peer.crt. The peer keeps its id, its zenoh identity and its
slot on the router. The daemon does not restart, and the router does not
restart.
The running link keeps the certificate it opened with. The cloud router closes that link when that certificate expires, and the daemon’s router opens it again at once with the renewed certificate.
The platform accepts a renewal from a signed-in person only, so the daemon uses the session of this machine:
| Condition | Result |
|---|---|
| A session is on the machine and the daemon runs | The daemon renews the certificate when the renewal is due. |
No session (you ran logout, or the refresh token expired) | The renewal fails. The daemon tries again each hour. Run peppy platform login. |
| The session is one of a different backend than the enrollment | The renewal fails, and the daemon log names the backend to sign in to. |
| The daemon does not run | No renewal. The daemon renews when it starts, also when the certificate expired. |
| The platform asks for a delay | The daemon tries again after that delay. |
| The platform cannot renew the peer (the peer was removed) | The daemon stops the renewal and the log names the remedy: peppy platform enroll --replace. |
peppy platform status shows the expiry of the certificate, the date from
which the daemon renews it, and what the daemon needs for the renewal and does
not have:
cert : expires on 2027-01-01 (in 80 days) renewal : automatic, from 2026-12-02 needs a session: run `peppy platform login`To mint a new certificate by hand, run:
peppy platform enroll --replaceThis enrolls anew (a new key, a new peer and a new zenoh identity), restarts the daemon under it, and removes the previous peer from the platform. The previous peer keeps its slot until the router restarts; see The project router.
Namespace
Section titled “Namespace”An enrolled daemon opens its own session, and every node it spawns, under the
project’s routing namespace. Two machines enrolled in the same project share a
namespace and address each other across the cloud router, which is the
foundation for running one stack across both; see
Federation. An unenrolled daemon runs under
local. The namespace is routing context only. It is not proof of project
membership, a tenant-isolation boundary, or an authorization policy: isolation
comes from each project having its own router and its own certificate
authority.
Checking the link
Section titled “Checking the link”peppy platform statusThe report has these parts: the enrollment on disk (project, peer, router,
certificate expiry and renewal, files), the daemon (running, under the enrolled
identity, and whether a fresh handshake to the cloud router validates), the
selection, and, when you are signed in, the platform’s view (the router’s
phase, the changes that wait for a restart, and this peer’s connection status). peppy platform peers lists
every peer of the project as the platform sees them, marking this machine.
A peer reads connected, pending_restart, or “not reported by the platform”.
The last one is not a failure: the platform could not read the sessions of the
router, so it has nothing to say about the peer. The daemon part of status
answers from this machine: it makes a TLS handshake with the router and says
if the link is good.
Unenrolling
Section titled “Unenrolling”peppy platform unenrollThis removes the peer on the platform, deletes ~/.peppy/conf/peer/, and pokes
the daemon, which restarts standalone under local. Removing the peer needs a
session; a machine that cannot sign in can delete only its local enrollment
with --local-only and have the peer removed from the web app. The same
restart prompt and --yes bypass apply as for enroll.
The cloud router reads its trust anchors when it starts. It refuses the removed
peer, and frees its slot, from its next restart. The platform does not restart
a router by itself: the command prints each change that waits and the router restart command that applies it.
The project router
Section titled “The project router”One project has one cloud router, so a project names its router.
peppy platform router restartpeppy platform router startWhich router a command acts on:
| Flags | Selected project | This machine | Router |
|---|---|---|---|
--project, --workspace | set or not | enrolled or not | The project the flags name, by id or exact name. |
| none | set | enrolled or not | The selected project. |
| none | not set | enrolled | The project this machine is enrolled in. |
| none | not set | not enrolled | None. The command stops and lists your projects. |
The command prints the router it found, its phase, its peers and the changes that wait, before it acts.
A restart applies every pending change: a removed peer, a new size. It drops
each peer link for a moment, and the peers connect again on their own. The
command asks for confirmation on a terminal; --yes skips the prompt.
start starts a router that was stopped. peppy platform status shows the
phase of the router and names this command when the router is stopped.
A stopped router takes two commands to apply its changes. The platform refuses
the restart of a stopped router, and a start alone does not apply a removal.
So router restart on a stopped router stops with the two commands to run, in
order: router start, then router restart. unenroll prints the same two
commands when the router is stopped.
Both commands need the permission to manage the infrastructure of the project. This is a different permission from the one an enrollment needs, so a person who can enroll a machine cannot always restart the router.
Federation outside the Peppy platform
Section titled “Federation outside the Peppy platform”You do not need the Peppy platform to run nodes across several machines. Point
every machine at a Zenoh router you run yourself with
zenoh.external
in ~/.peppy/conf/peppy_config.json5:
zenoh: { external: { endpoint: "tcp/router.internal:7448", },},endpoint is a dial address, so start the router first (for example
zenohd -l tcp/0.0.0.0:7448) and make sure every machine, and every node
environment on it, can reach that host and port. Peppy dials this router, adopts
it, and never starts, restarts, reconfigures, or federates it: its lifecycle is
yours. No sign-in, no backend, and no cloud router is involved.
Every session carries a namespace under zenoh.external exactly as it does
under zenoh.managed: local when the machine is not enrolled, the project
namespace when it is. A shared router forwards traffic between namespaces
without matching it, so an enrolled machine and an unenrolled one discover
nothing from each other even though both are connected to your router. Keep
every machine on the same namespace; without the platform that is local, so
unenroll each machine (peppy platform unenroll --local-only when it has no
session) and restart its daemon. External mode has no federation control
socket, so enroll and unenroll never restart the daemon: a namespace change
reaches sessions only when you restart it by hand (peppy service stop, then
start it again).
Backend
Section titled “Backend”By default the CLI talks to the prod backend, https://api.peppy.bot. That URL
is stored in the resource_servers block of
~/.peppy/conf/peppy_config.json5 (see Daemon configuration),
seeded on first run and editable in place. To point at a different backend
without editing the file, use --api-url or PEPPY_API_URL.
The URL is resolved in precedence order: --api-url, then PEPPY_API_URL, then
resource_servers.api. In release builds, plain http is allowed only for
local backends (loopback / *.localhost); anything else must be https. Debug
builds accept remote plain http with a warning.
Credential storage
Section titled “Credential storage”Tokens live at ~/.peppy/conf/credentials.json5, written owner-only (0600).
The selection lives beside it in platform_selection.json5; it holds ids and
names, no secret. The enrollment lives under ~/.peppy/conf/peer/, with the
private key and the record owner-only. The root honors PEPPY_HOME.
Tokens are never printed and Authorization headers are redacted in verbose
output.
Signing out
Section titled “Signing out”peppy platform logoutThis revokes the refresh token and then the access token at the identity provider, then deletes the local session. The backend checks every bearer with the identity provider, so a revoked token stops working at once. It revokes only the tokens this machine holds; a session on another device keeps working.
Signing out removes the selection with the session. It leaves the enrollment in
place: the daemon keeps its certificate and its link to the project router.
The CLI’s platform calls need a new peppy platform login, and so does the
renewal of the certificate: with no session the daemon cannot renew it, and
the link stops when the certificate expires. On an enrolled machine, logout
prints that expiry date.
Environment variables
Section titled “Environment variables”| Variable | Purpose |
|---|---|
PEPPY_API_URL | Override the backend base URL. |
PEPPY_HOME | Override the ~/.peppy data root (also moves the credentials file and the enrollment). |
NO_COLOR | Disable colored output. |
Running one stack across your project’s machines
Section titled “Running one stack across your project’s machines”Once two machines are enrolled in the same project they share a namespace and can address each other through the project’s cloud router, which is the foundation for running a single Peppy stack across both. See Federation.