Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
# 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](https://github.com/pubky/pubky-homeserver) SDK.

## Install

```toml
# Cargo.toml
[dependencies]
pubky = "0.x"            # this crate
# Optional helpers used in examples:
# pubky-testnet = "0.x"
```

## Quick start

```rust no_run
use pubky::prelude::*;
use pubky::ClientId;

# async fn run() -> pubky::Result<()> {

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);

# Ok(()) }
```

## 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:

```rust,no_run
use pubky::{Pubky, PubkyHttpClient};

let client = PubkyHttpClient::builder()
    .max_error_body_bytes(1024)
    .build()?;
let pubky = Pubky::with_client(client);
# Ok::<_, pubky::BuildError>(())
```

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:

```rust,no_run
# async fn example(pubky: pubky::Pubky, user: pubky::PublicKey) -> pubky::Result<()> {
let stream = pubky.event_stream_for_user(&user, None)
    .max_event_bytes(8192)
    .live()
    .subscribe()
    .await?;
# Ok(()) }
```

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. Can `signup`, `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 `_pubky` records.

#### Transport:

- **`PubkyHttpClient`** : handles requests to pubky public-key hosts.

## Examples

### Storage API (session & public)

Session (authenticated):

```rust no_run
use pubky::{ClientId, Keypair, Pubky};

# async fn run(keypair: Keypair) -> pubky::Result<()> {

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?;

# Ok(()) }
```

Public (read-only):

```rust no_run
use pubky::{Pubky, PublicKey};

# async fn run(user_id: PublicKey) -> pubky::Result<()> {

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());
}

# Ok(()) }
```

See the [Public Storage example](https://github.com/pubky/pubky-homeserver/tree/main/examples/rust/3-storage).

Path rules:

- Session storage uses **absolute** paths under `/pub/` or `/priv/`.
- Public storage uses **addressed** form `pubky<user>/pub/app/file.txt` (preferred) or `pubky://<user>/...`.
- A storage path cannot be both an exact file and an implicit folder prefix. For example:
  - if `/pub/app/foo` exists, writing `/pub/app/foo/bar.json` returns `409 Conflict`.
  - if descendants under `/pub/app/foo/` exist, writing `/pub/app/foo` returns `409 Conflict`.

**Convention:** put your app’s public data under a domain-like folder in `/pub`, e.g. `/pub/my-new-app/`.

See [Private Storage](../docs/PRIVATE_STORAGE.md) 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:

```rust no_run
# use pubky::{PubkyHttpClient, resolve_pubky};
# use reqwest::Method;
# async fn run() -> pubky::Result<()> {
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?;
# Ok(())
# }
```

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.

```rust no_run
use pubky::{Pubky, PublicKey, Keypair};
# async fn run(other: PublicKey, new_homeserver_id: PublicKey) -> pubky::Result<()> {
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?;

# Ok(()) }
```

### Pubky QR auth for third-party and keyless apps

Request an authorization URL and await approval.

**Typical usage:**

1. Start an auth flow with `pubky.start_grant_auth_flow(&caps, ..)`. You can also use `PubkyGrantAuthFlow::builder()` to set a custom relay. The deprecated cookie flow remains available only for compatibility.
2. Show `authorization_url()` (QR/deeplink) to the signing device (e.g., [Pubky Ring]https://github.com/pubky/pubky-ring — [iOS]https://apps.apple.com/om/app/pubky-ring/id6739356756 / [Android]https://play.google.com/store/apps/details?id=to.pubky.ring).
3. Await `await_approval()` to obtain a session-bound `PubkySession`, or `await_credential()` for a raw `GrantCredential`/`CookieCredential` that you can persist, inspect, or lift into a session later via `PubkySession::from_{grant,cookie}_credential`.

```rust
# use pubky::{AuthFlowKind, Capabilities, ClientId, Keypair, Pubky};
# async fn auth() -> pubky::Result<()> {

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?;
# pubky.signer(Keypair::random()).approve_auth(flow.authorization_url()).await?;

let session = flow.await_approval().await?;

# Ok(()) }
```

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:

```rust
# use pubky::{AuthFlowKind, Capabilities, ClientId, PubkyGrantAuthFlow};
# use pubky::deep_links::XCallbackParams;
# fn callback_flow() -> pubky::Result<()> {
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()?;
# drop(flow);
# Ok(()) }
```

Approve an auth request

```rust ignore
signer.approve_auth(authorization_url).await?;
```

`approve_auth()` approves every requested capability. To approve fewer scopes
or actions, use `approve_auth_with_capabilities()`:

```rust ignore
// 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][select-capabilities] for
validation rules, empty approvals, and encryption key delivery.

[select-capabilities]:
  ../docs/scoped-encryption-keys.md#select-capabilities-on-the-signing-device

See the fully functional [**Auth Flow Example**](https://github.com/pubky/pubky-homeserver/tree/main/examples/rust/2-auth_flow).

#### 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]https://httprelay.io.
  The channel is derived as `base64url(hash(secret))`; the token is end-to-end encrypted with the `secret` and cannot be decrypted by the relay.

**Custom relay example**

```rust
# use pubky::{Pubky, PubkyGrantAuthFlow, Capabilities, AuthFlowKind, ClientId};
# async fn custom_relay() -> pubky::Result<()> {
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()?;
# Ok(()) }
```

> 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.

```rust no_run
# use pubky::{AuthFlowKind, Capabilities, ClientId, PubkyGrantAuthFlow, StoragePath};
# use pubky::deep_links::GrantApprovalFormat;
# async fn keys() -> Result<(), Box<dyn std::error::Error>> {
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")?)?;
}
# Ok(()) }
```

Keys stay usable after the grant expires or is revoked, and older homeservers
reject grants containing `e`. Read the
[scoped encryption keys guide](../docs/scoped-encryption-keys.md) 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`: enable `Storage` helpers (`.get_json()` / `.put_json()`) and serde on certain types.

```toml
# 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:

```rust no_run
# use pubky_testnet::{EphemeralTestnet, pubky::{ClientId, Keypair}};
# async fn test() -> pubky_testnet::pubky::Result<()> {

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");

# Ok(()) }
```

## Keypair and Session persistence

Encrypted Keypair secrets (`.pkarr`):

```rust no_run
use pubky::Pubky;
# fn run() -> pubky::Result<()> {
let pubky = Pubky::new()?;
let signer = pubky.signer_from_recovery_file("/path/to/alice.pkarr", "passphrase")?;
# Ok(()) }
```

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.

```rust no_run
use pubky::{ClientId, Pubky};
# async fn run() -> pubky::Result<()> {
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?;

# Ok(()) }
```

The deprecated `.sess` helpers are cookie-only; [`write_secret_file`](PubkySession::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](https://github.com/pubky/pubky-homeserver/tree/main/examples) using the Pubky SDK.

## JS bindings

Find a wrapper of this crate using `wasm_bindgen` in [npmjs.com](https://www.npmjs.com/package/@synonymdev/pubky). Or build on `pubky-sdk` codebase under `pubky-sdk/bindings/js`.

---

**License:** MIT
**Relay:** [https://httprelay.io](https://httprelay.io) (open source; run your own for production)