Identity System
The identity system in the Pico Engine is available in versions 1.6 and higher.
The pico-engine identity model separates who you are (human access to your mesh), what your picos are (pico-to-pico identity), and who third parties are (external apps and webhooks). Each layer uses a different mechanism, but they work together: you sign in with a passkey, your root pico acts as your agent in the mesh, and external callers use OAuth to reach the public API.
This page is the overview. Detailed guides live on the child pages linked below.
Three planes of identity
Plane | Question it answers | Mechanism | Status |
|---|---|---|---|
Human → root pico | Is this the right person for this account? | Passkeys (WebAuthn) | Available |
Pico → pico | Which pico is calling, and is it allowed? | DID/DIDComm + channel policy | Available |
External app → mesh | May this third party call the public API? | OAuth 2.0 | Available |
Human → root pico (passkeys)
Every account is tied to a root pico. When you register or sign in, you use a passkey — a device-backed WebAuthn credential (Touch ID, Windows Hello, a security key, etc.). There is no shared password and no central user database: each root pico is its own WebAuthn relying party, and the passkey proves you control that account.
The root pico is your agent in the system. You authenticate to it; it holds your mesh, rulesets, and cryptographic identity for pico-to-pico communication.
→ See: Passkey authentication
Pico → pico (DID/DIDComm)
Since pico-engine 1.6, every pico has a portable did:webvh identity that provides the answer to “which pico is this?” for introductions, discovery, and (eventually) verifiable credentials. When two unrelated picos form a relationship, they run a SKY intro handshake; after approval, the relationship stores pairwise did:peer DIDs for ongoing encrypted traffic.
Three identifiers show up in practice:
Identifier | Role |
|---|---|
did:webvh | Portable pico identity — share this when someone wants to form a relationship with you ( |
did:peer | Private relationship identity — minted during intro; used for queries and events after the relationship is established |
ECI | Local channel on this engine — relationship Rx policy is enforced here (classic ECI-based relationships also use a remote Tx ECI) |
DID-based relationships are the default for new work: raise wrangler:relationship with layer2: true and target_did set to the recipient’s did:webvh (not a peer DID, not a wellKnown ECI). ECI-based relationships — the classic wellKnown_Tx handshake — remain fully supported for existing rulesets and tutorials.
This plane is separate from how humans sign in: you use a passkey to reach your mesh; picos use DIDs and channel policy to talk to each other. It is also separate from OAuth: external apps present Bearer tokens on /sky/*; pico-to-pico traffic uses SKY over DIDComm when picos are on different meshes or engines, and stays on verified local dispatch when both picos share a mesh on the same engine.
Root picos default to public intro (accept unsolicited SKY intro on did:webvh); child picos default to off. Toggle in the developer UI Relationships tab or with wrangler:set_public_intro.
Details: Relationships (formation, lifecycle, routing, cross-engine)
Deprecated: io.picolabs.did-o — use wrangler:picoQuery and event:send with a DID or an established relationship map.
Short enough for an overview; the relationships page carries the how-to. Say if you want it tighter (drop the table or the deprecated line).
External app → mesh (OAuth)
Third parties — Home Assistant, inbound webhook senders, future integrators — cannot use your passkey session. They authenticate with OAuth 2.0 and present a Bearer token on the public /sky/* API.
Two grant types cover the two main external patterns:
Grant | Typical use | Scope |
|---|---|---|
Client Credentials | Inbound webhooks (Helium, Stripe, …) | Single channel (one ECI) |
Authorization Code + PKCE | User-connected apps (Home Assistant, dashboards) | Whole mesh under a root |
Both can coexist in the same mesh. They share the same token store and Bearer middleware; validation differs by grant type.
→ See: OAuth — Client Credentials (webhooks)
→ See: OAuth — Authorization Code Grant (apps)
Two HTTP surfaces
The engine exposes two channel HTTP prefixes. Treat them as different audiences, not interchangeable paths.
Prefix | Audience | Authentication |
|---|---|---|
| Engine UI, in-engine KRL HTTP loops | Passkey session cookie |
| External callers (webhooks, integrators, Manifold-style apps) | OAuth Bearer token when required; otherwise channel policy only |
Rule of thumb: If you are building something outside the engine — a webhook receiver, Home Assistant, a mobile app — use /sky/* only. Never depend on /c/* for third-party access.
When OAuth is required on /sky/*
Bearer tokens are enforced on /sky/* when either:
The channel was created with the
oauth-webhooktag (webhook Client Credentials), orThe optional ruleset (part of Wrangler)
io.picolabs.oauthis installed on the root pico (mesh lock).
Without mesh lock, open channels continue to work under channel policy alone. Installing io.picolabs.oauth on the root requires a valid Bearer token on all /sky/* requests for that mesh — the standard setup for Authorization Code apps and for locking down external access.
How the pieces fit together
Human Engine External caller
│ │ │
│ passkey (WebAuthn) │ │
├────────────────────────►│ session cookie on /c/* │
│ │ (UI, settings, channel mgmt) │
│ │ │
│ │◄─────────────────────────────────┤
│ │ Bearer oat_… on /sky/* │
│ │ (OAuth — CC or ACG) │
│ │ │
│ consent (ACG only) │ │
├────────────────────────►│ /oauth/authorize → approve │
│ │ │You register or sign in with a passkey → the engine issues a session used by the developer UI and session-gated management APIs.
You (or your automation) create webhook credentials or register OAuth apps from the UI while signed in.
External systems exchange credentials at
POST /oauth/tokenand call/sky/*withAuthorization: Bearer <token>.For Authorization Code apps, the user must sign in and approve access on a consent page before the app receives tokens.
Channel ECI stays in the URL for routing (/sky/query/<eci>/…). Access tokens are opaque bearer strings (oat_…); they are not the ECI itself.
Accounts, roots, and access control
One account, one root
Each registration creates an account and a root pico (1:1). The first registration on a fresh engine is bootstrap — no separate setup token. Additional accounts require either open self-signup (environment variable) or an invite link from an existing user.
Legacy engines with a root but no auth accounts can claim the existing root with a passkey instead of creating a new one.
Admin vs conferred rights
Admin rights follow the pico parent–child tree: control over your root and its descendants.
Other rights (read access, specific events, etc.) are conferred through policy — channel rules, relationship tags, and (eventually) verifiable credentials — without requiring admin status in the hierarchy.
Passkey login establishes who you are. The hierarchy and channel policy determine what you (or a token) may do.
Choosing the right mechanism
I want to… | Use |
|---|---|
Use the developer UI | Passkey sign-in (session on |
Let a webhook POST events to one channel | Client Credentials on that channel ( |
Let an app query many picos under my root | Install |
Call the engine from KRL on the same host |
|
Lock all external API access on my mesh | Install |
Configuration
Identity-related environment variables:
PICO_ENGINE_BASE_URL— public URL for passkey relying-party origin and OAuth redirects (must match what users see in the browser).PICO_ENGINE_ALLOW_SELF_SIGNUP— allow open registration after bootstrap (true/1).PICO_ENGINE_ALLOW_LOCALHOST_C— set to0to require a passkey session on/c/*even from localhost (default allows localhost for in-engine HTTP loops).
See the Environment variables page for the full list.