Expand description
§Pubky SDK
Ergonomic building blocks for Pubky apps: one facade (Pubky) plus focused actors for sessions, storage API, signer helpers, and QR auth flow for keyless apps.
Rust implementation of Pubky SDK.
§Install
# Cargo.toml
[dependencies]
pubky = "0.x" # this crate
# Optional helpers used in examples:
# pubky-testnet = "0.x"§Quick start
use pubky::prelude::*;
use pubky::ClientId;
let pubky = Pubky::new()?; // or Pubky::testnet() for local testnet.
// 1) Create a new random key user and bound to a Signer
let keypair = Keypair::random();
let signer = pubky.signer(keypair);
// 2) Sign up on a homeserver (identified by its public key)
let homeserver = PublicKey::try_from("o4dksf...uyy").unwrap();
signer.signup(&homeserver, None).await?;
let session = signer.signin(ClientId::new("my-cool-app").unwrap()).await?;
// 3) Read/Write as the signed-in user
session.storage().put("/pub/my-cool-app/hello.txt", "hello").await?;
let body = session.storage().get("/pub/my-cool-app/hello.txt").await?.text().await?;
assert_eq!(&body, "hello");
// 4) Public read of another user’s file
let txt = pubky
.public_storage()
.get(format!(
"{}/pub/my-cool-app/hello.txt",
session.info().public_key()
))
.await?
.text().await?;
assert_eq!(txt, "hello");
// 5) Keyless app flow (QR/deeplink)
let caps = Capabilities::builder()
.write("/pub/example.com/")
.expect("static scope is canonical")
.finish();
let flow = pubky.start_grant_auth_flow(
&caps,
AuthFlowKind::signin(),
ClientId::new("my-cool-app").unwrap(),
)?;
println!("Scan to sign in: {}", flow.authorization_url());
let app_session = flow.await_approval().await?;
// 6) Optional (advanced): publish or resolve PKDNS (_pubky) records
signer.pkdns().publish_homeserver_if_stale(None).await?;
let resolved = signer.pkdns().get_homeserver().await;
println!("Your current homeserver: {:?}", resolved);
§Error-body limits
HTTP status checking leaves successful response bodies unread. For HTTP errors, the client captures up to 4096 body bytes for the error message. Change this limit when creating the client:
use pubky::{Pubky, PubkyHttpClient};
let client = PubkyHttpClient::builder()
.max_error_body_bytes(1024)
.build()?;
let pubky = Pubky::with_client(client);The setting applies to checked requests made through that client except
credential refresh. Refresh HTTP failures always skip and cancel response
bodies regardless of max_error_body_bytes. Refresh request failures surface
as Error::CredentialRefresh(CredentialRefreshError). Other error categories
keep their original variants. The refresh request error retains an optional
HTTP status but redacts HTTP response bodies and request URLs from Display
and Debug. Other checked request errors use the captured message. Set the
limit to 0 to skip error-body reads and use the HTTP status reason as that
message. Positive limits preserve short messages and mark longer ones with
\n[response body truncated at N bytes].
The limit caps captured response bytes. Invalid UTF-8 can expand the message to three times that size, plus the truncation marker. Transport buffers and allocations add memory overhead.
In JavaScript, use Pubky.withClient(new Client({ maxErrorBodyBytes: 1024 })).
§Event-stream limits
Event subscriptions use sse-core with a default limit of usize::MAX,
which is effectively unbounded.
Set a client-side limit per subscription to reject oversized or unterminated
SSE payloads in historical and live streams:
let stream = pubky.event_stream_for_user(&user, None)
.max_event_bytes(8192)
.live()
.subscribe()
.await?;In JavaScript, use .maxEventBytes(8192) on the event-stream builder.
Rust rejects zero at subscription time; JavaScript requires an integer in
1..=4294967295 when setting the limit.
The limit applies separately to accumulated event data, each event name, and
each ID. Data includes the newlines joining
data fields; repeated event and id fields replace their previous values.
Comments and unknown fields are skipped without buffering their contents.
Field prefixes, framing line endings and a leading UTF-8 BOM do not count.
There is no total block or stream byte limit. This setting is independent of
.limit() (event count) and max_error_body_bytes.
On overflow, the SDK closes the response and yields one validation error, then
ends the Rust stream or errors the JavaScript ReadableStream. It still skips
malformed Pubky events and unknown event types within the limit.
All subscriptions replace invalid UTF-8 and discard incomplete events at EOF. Transport errors release the response and end the stream. This changes the previous default parser’s behavior, which reported UTF-8 errors and could continue after a transport error.
Parser memory is a multiple of the limit because data, event names and IDs have separate buffers, with additional UTF-8 replacement and URL encoding overhead. Transport buffers and events retained by the application are outside this limit.
§Key formats (display vs transport)
PublicKey has two string representations:
- Display format:
pubky<z32>(used for logs/UI and human-facing identifiers). - Transport/storage format: raw
z32(used for hostnames, storage owner path segments, legacy headers, query params, serde/JSON, and database storage).
Use .z32() whenever you are building hostnames or transport values (for example _pubky.<z32>, /storage/<z32>/..., or the legacy pubky-host header). Use Display/.to_string() when you want the prefixed identifier for people.
§Reuse a single facade across your app
Use a shared Pubky (via cloning it, passing down as argument or behind OnceCell) instead of constructing one per request. This avoids reinitializing transports and keeps the same client available for repeated usage.
§Mental model
Pubky- facade, always start here! Owns the transport and constructs actors.PubkySigner- local key holder. Cansignup,signin, approve QR auth, publish PKDNS.PubkySession- authenticated “as me” handle. Exposes session-scoped storage.GrantManager- account-level grant listing and revocation using an authenticated root session.PublicStorage- unauthenticated reads of others’ public data.Pkdns- resolve/publish_pubkyrecords.
§Transport:
PubkyHttpClient: handles requests to pubky public-key hosts.
§Examples
§Storage API (session & public)
Session (authenticated):
use pubky::{ClientId, Keypair, Pubky};
let pubky = Pubky::new()?;
let session = pubky
.signer(keypair)
.signin(ClientId::new("my-cool-app").unwrap())
.await?;
let storage = session.storage();
storage.put("/pub/my-cool-app/data.txt", "hi").await?;
let text = storage.get("/pub/my-cool-app/data.txt").await?.text().await?;
Public (read-only):
use pubky::{Pubky, PublicKey};
let pubky = Pubky::new()?;
let public = pubky.public_storage();
let file = public
.get(format!("{user_id}/pub/example.com/file.bin"))
.await?
.bytes()
.await?;
let entries = public
.list(format!("{user_id}/pub/example.com/"))?
.limit(10)
.send()
.await?;
for entry in entries {
println!("{}", entry.to_pubky_url());
}
See the Public Storage example.
Path rules:
- Session storage uses absolute paths under
/pub/or/priv/. - Public storage uses addressed form
pubky<user>/pub/app/file.txt(preferred) orpubky://<user>/.... - A storage path cannot be both an exact file and an implicit folder prefix. For example:
- if
/pub/app/fooexists, writing/pub/app/foo/bar.jsonreturns409 Conflict. - if descendants under
/pub/app/foo/exist, writing/pub/app/fooreturns409 Conflict.
- if
Convention: put your app’s public data under a domain-like folder in /pub, e.g. /pub/my-new-app/.
See Private Storage for /priv/ access and events.
§Resolve identifiers into transport URLs
Use resolve_pubky to turn a public resource identifier into its canonical HTTPS homeserver URL. PubkyHttpClient::request_async resolves the transport and falls back to legacy storage addressing when needed:
let client = PubkyHttpClient::new()?;
let url = resolve_pubky("pubkyoperrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/pub/pubky.app/posts/0033X02JAN0SG")?;
assert_eq!(
url.as_str(),
"https://_pubky.operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/storage/operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/pub/pubky.app/posts/0033X02JAN0SG"
);
let response = client.request_async(Method::GET, url).await?.send().await?;The high-level Rust storage APIs and JavaScript Client.fetch do this automatically, so their storage requests remain compatible with older homeservers. Other HTTP clients can send the canonical /storage/{owner}/... URL only to homeservers that advertise path-addressed-storage.
§PKDNS (Pkarr)
Resolve another user’s homeserver (_pubky record), or publish your own via the signer.
use pubky::{Pubky, PublicKey, Keypair};
let pubky = Pubky::new()?;
// read-only homeserver resolver
let host: Option<PublicKey> = pubky.get_homeserver_of(&other).await?;
// publish with your key
let signer = pubky.signer(Keypair::random());
signer.pkdns().publish_homeserver_if_stale(None).await?;
// or force republish (e.g. homeserver migration)
signer.pkdns().publish_homeserver_force(Some(&new_homeserver_id)).await?;
// resolve your own homeserver
signer.pkdns().get_homeserver().await?;
§Pubky QR auth for third-party and keyless apps
Request an authorization URL and await approval.
Typical usage:
- Start an auth flow with
pubky.start_grant_auth_flow(&caps, ..). You can also usePubkyGrantAuthFlow::builder()to set a custom relay. The deprecated cookie flow remains available only for compatibility. - Show
authorization_url()(QR/deeplink) to the signing device (e.g., Pubky Ring — iOS / Android). - Await
await_approval()to obtain a session-boundPubkySession, orawait_credential()for a rawGrantCredential/CookieCredentialthat you can persist, inspect, or lift into a session later viaPubkySession::from_{grant,cookie}_credential.
let pubky = Pubky::new()?;
// Read/Write capabilities for acme.app route
let caps = Capabilities::builder()
.read_write("/pub/example.com/")
.expect("static scope is canonical")
.finish();
// Start the flow using the default relay (see “Relay & reliability” below)
let flow = pubky.start_grant_auth_flow(
&caps,
AuthFlowKind::signin(),
ClientId::new("example.com").expect("static client id is valid"),
)?;
println!("Scan to sign in: {}", flow.authorization_url());
// On the signing device, approve with: signer.approve_auth(flow.authorization_url()).await?;
let session = flow.await_approval().await?;
Optional x-callback parameters let the authenticator return control to the requesting app after it has handled the deep link. They work with both cookie and grant flows:
let caps = Capabilities::builder()
.read_write("/pub/example.com/")
.expect("static scope is canonical")
.finish();
let callbacks = XCallbackParams {
x_source: Some("Example App".into()),
x_success: Some("example://auth/success?nonce=unique".into()),
x_error: Some("example://auth/error?nonce=unique".into()),
x_cancel: Some("example://auth/cancel?nonce=unique".into()),
};
let flow = PubkyGrantAuthFlow::builder(
&caps,
AuthFlowKind::signin(),
ClientId::new("example.com").expect("static client id is valid"),
)
.x_callback(callbacks)
.start()?;Approve an auth request
signer.approve_auth(authorization_url).await?;approve_auth() approves every requested capability. To approve fewer scopes
or actions, use approve_auth_with_capabilities():
// The request includes `/pub/example.app/:rwe`; the user approves only read.
let approved = Capabilities::builder().read("/pub/example.app/")?.finish();
signer
.approve_auth_with_capabilities(authorization_url, &approved)
.await?;See selecting capabilities on the signing device for validation rules, empty approvals, and encryption key delivery.
See the fully functional Auth Flow Example.
§Relay & reliability
- If you don’t specify a relay, the auth flow defaults to a Synonym-hosted relay. If that relay is down, logins won’t complete.
- For production and larger apps, run your own relay (MIT, Docker): https://httprelay.io.
The channel is derived as
base64url(hash(secret)); the token is end-to-end encrypted with thesecretand cannot be decrypted by the relay.
Custom relay example
let pubky = Pubky::new()?;
let caps = Capabilities::builder()
.read("/pub/example.com/")
.expect("static scope is canonical")
.finish();
let client_id = ClientId::new("my.app").unwrap();
let auth_flow = PubkyGrantAuthFlow::builder(&caps, AuthFlowKind::signin(), client_id)
.client(pubky.client().clone())
.relay(url::Url::parse("http://localhost:8080/inbox/")?) // your relay
.start()?;Tip: reuse
pubky.client()when customising the relay so the flow shares TLS and pkarr configuration with the rest of your application.
§Request encryption keys
Apps can also ask the signer for encryption keys scoped to storage paths. Add
the e action to each scope that needs keys and select the signed approval
format. Storage actions r and w don’t deliver keys.
let caps = Capabilities::builder()
.read_write("/pub/chat/")?
.encryption_keys("/pub/chat/")?
.finish();
let flow = PubkyGrantAuthFlow::builder(
&caps,
AuthFlowKind::signin(),
ClientId::new("chat.example").expect("static client id is valid"),
)
.approval_format(GrantApprovalFormat::SignedApprovalV1)
.start()?;
let session = flow.await_approval().await?;
let grant = session.as_grant().expect("grant flows create grant sessions");
if let Some(keys) = grant.encryption_keys() {
let key = keys.derive_for_path(&StoragePath::new("/pub/chat/message")?)?;
}Keys stay usable after the grant expires or is revoked, and older homeservers
reject grants containing e. Read the
scoped encryption keys guide before you
use them. For details and offline key recovery, see the rustdoc of
GrantSessionView::encryption_keys and GrantCredential::restore_encryption_keys.
When you recover keys from saved material, check that they belong to the user
and grant you expect.
§Features
json: enableStoragehelpers (.get_json()/.put_json()) and serde on certain types.
# Cargo.toml
[dependencies]
pubky = { version = "x.y.z", features = ["json"] }§Testing locally
Spin up an ephemeral testnet (DHT + homeserver + relay) and run your tests fully offline:
let testnet = EphemeralTestnet::builder().build().await.unwrap();
let homeserver = testnet.homeserver_app();
let pubky = testnet.sdk()?;
let signer = pubky.signer(Keypair::random());
signer.signup(&homeserver.public_key().into(), None).await?;
let session = signer
.signin(ClientId::new("my-cool-app").unwrap())
.await?;
session.storage().put("/pub/my-cool-app/hello.txt", "hi").await?;
let s = session.storage().get("/pub/my-cool-app/hello.txt").await?.text().await?;
assert_eq!(s, "hi");
§Keypair and Session persistence
Encrypted Keypair secrets (.pkarr):
use pubky::Pubky;
let pubky = Pubky::new()?;
let signer = pubky.signer_from_recovery_file("/path/to/alice.pkarr", "passphrase")?;Grant session secrets:
signin creates a grant-backed session. Export its secret with
GrantSessionView::export_local_secret and restore it with Pubky::restore_session.
This example loads the keypair of an account that has already signed up.
use pubky::{ClientId, Pubky};
let pubky = Pubky::new()?;
let signer = pubky.signer_from_recovery_file("/path/to/alice.pkarr", "passphrase")?;
let session = signer
.signin(ClientId::new("my-cool-app").unwrap())
.await?;
let grant = session
.as_grant()
.expect("signin creates a grant-backed session");
let secret = grant
.export_local_secret()
.await
.expect("signin creates an exportable local PoP key");
// Store `secret` securely, then load it when the application restarts.
let restored = pubky.restore_session(&secret).await?;
The deprecated .sess helpers are cookie-only; write_secret_file
panics on grant sessions.
Security: the exported secret is unencrypted and contains the grant, its private proof-of-possession key, and, for signed approvals, the approval with its encryption keys. Anyone holding it can authenticate within the granted capabilities until the grant expires or is revoked. The encryption keys stay usable after that. Store it securely and never log it.
§Request errors
errors::RequestError::kind classifies a failure as an HTTP status, timeout,
connection, body, decoding or invalid-request error, or Other when the
transport gives no more detail (browser Fetch exposes less than native). The
category carries no URL, body or credentials; the original error keeps the
diagnostics. A category describes what was observed, not whether a retry is
safe: a timeout does not prove a write was not committed.
§Example code
Check more examples using the Pubky SDK.
§JS bindings
Find a wrapper of this crate using wasm_bindgen in npmjs.com. Or build on pubky-sdk codebase under pubky-sdk/bindings/js.
License: MIT Relay: https://httprelay.io (open source; run your own for production)
Modules§
- custom_
pop - Custom proofs signed by a grant’s client key, with offline verification.
- deep_
links - Deep link related module. Contains the following:
- errors
- Unified error types for the
pubkycrate. - pkarr
- Pkarr
- prelude
- Common imports for quick starts.
- recovery_
file - Tools for encrypting and decrypting a recovery file storing user’s root key’s secret.
Macros§
- cross_
log - Cross-platform logging macro with explicit level selection.
Structs§
- Auth
Token - Authentication token used by the Pubky Auth protocol.
- Capabilities
- A wrapper around
Vec<Capability>that controls how capabilities are serialized and built. - Capability
- A single capability: a
scopeand the allowedactionswithin it. - Client
Id - An application identifier, typically a domain string (e.g.,
franky.pubky.app). - Cookie
Credential Deprecated - Cookie-based session credential (legacy).
- Cookie
Session Record - Cookie-specific session record with binary (postcard) serialization.
- Cookie
Session View Deprecated - Cookie-only operations on a
PubkySession. - Delegated
Grant Auth Flow State - Serializable state for resuming a pending delegated browser grant auth flow.
- Delegated
Grant Credential State - Non-secret durable metadata for browser delegated grant restore.
- Encrypted
Http Relay Inbox Channel - An encrypted HTTP relay inbox channel that encrypts/decrypts messages using a shared secret, with store-and-forward semantics.
- Event
- A single event from the event stream.
- Event
Cursor - Cursor for pagination in event queries.
- Event
Stream Builder - Builder for creating an event stream subscription.
- Grant
Auth Flow State - Serializable state for resuming a pending grant auth flow.
- Grant
Claims - Grant JWS claims — the serializable JWS claims.
- Grant
Credential - Cheap-to-clone grant credential. The mutable token state is shared across
clones via
Arc<Mutex<…>>so everyPubkySessionclone observes the same refreshes. Session info is derived from the immutable grant and never changes. - Grant
Info - Summary of an active grant returned by
GET /auth/grant/sessions. - Grant
Manager - Account-level grant management for a signed-in user.
- Grant
Session Info - Session metadata returned alongside the bearer.
- Grant
Session Response - Response from
POST /sessionfor grant-based authentication. - Grant
Session View - grant-only operations on a
PubkySession. - Http
Relay Inbox Channel - An HTTP relay inbox channel for store-and-forward messaging.
- Keypair
- Wrapper around
pkarr::Keypairthat customizesPublicKeyrendering. - List
Builder - Unified builder for homeserver
LISTqueries (works for session & public). - Method
- The Request Method (VERB)
- Pkdns
- PKDNS actor: resolve & publish
_pubkyPKARR records. - PopProof
Claims - Proof-of-Possession JWS claims — the serializable JWS claims.
- Pubky
- High-level facade — your entry point to the Pubky SDK.
- Pubky
Cookie Auth Flow Deprecated - End-to-end legacy (cookie) auth flow handle.
- Pubky
Grant Auth Flow - End-to-end Grant +
PoPauth flow handle. - Pubky
Http Client - Transport client for Pubky homeserver APIs and generic HTTP, with PKARR-aware URL handling.
- Pubky
Http Client Builder - Configures a
PubkyHttpClientbefore construction. - Pubky
Resource - An addressed resource:
(owner: PublicKey, path: ResourcePath). - Pubky
Session - Your authenticated handle after signing in.
- Pubky
Signer - Holds a private key and proves identity.
- Public
Key - Wrapper around
pkarr::PublicKeythat renders with thepubkyprefix. - Public
Storage - Read anyone’s public data without signing in (unauthenticated).
- Resource
Path - An absolute, URL-safe homeserver path (
/…), with percent-encoding where needed. - Resource
Stats - Typed metadata for a stored resource, extracted from
HEADresponse headers. - Session
Info - Minimal, auth-agnostic session metadata.
- Session
Storage - Read and write your own data with simple path-based operations (authenticated).
- Status
Code - An HTTP status code (
status-codein RFC 9110 et al.). - Storage
Lock - An exclusive write lock on one file path, granted by the homeserver.
- Storage
Path - A canonical decoded Pubky storage path that can be encoded as an HTTP/WebDAV URI path.
Enums§
- Auth
Flow Kind - The kind of authentication flow to perform.
- Build
Error - Errors that can occur while configuring or building a
crate::PubkyHttpClient. - Credential
Refresh Error - Secret-safe provenance for a failed grant credential refresh request.
- Error
- The crate’s top-level error type.
- Event
Type - Type of event in the event stream.
- Storage
Path Error - Error validating or normalizing a
StoragePath.
Constants§
- DEFAULT_
HTTP_ RELAY Deprecated - Default HTTP relay base when none is supplied.
- DEFAULT_
HTTP_ RELAY_ INBOX - Default HTTP relay inbox base when none is supplied.
- DEFAULT_
STALE_ AFTER - Default staleness window for homeserver
_pubkyPkarr records (1 hour). - GRANT_
JWS_ TYP - JWS header
typfor Grant tokens. - POP_
JWS_ TYP - JWS header
typfor Proof-of-Possession proofs.
Traits§
- Into
Pubky Resource - Convert common input types into a normalized, addressed
PubkyResource. - Into
Resource Path - Convert common input types into a normalized
ResourcePath(absolute).
Functions§
- resolve_
pubky - Resolve a Pubky identifier (either
pubky://orpubky<pk>/…) into a transport URL.