@synonymdev/pubky
v0.6.0
Published
Pubky client
Readme
Pubky
JS/WASM SDK for Pubky.
Works in browsers and Node 20+.
Install
npm install @synonymdev/pubkyNode: requires Node v20+ (undici fetch, WebCrypto).
Module system + TS types: ESM and CommonJS both supported; TypeScript typings generated via tsify are included. Use import { Pubky } from "@synonymdev/pubky" (ESM) or const { Pubky } = require("@synonymdev/pubky") (CJS).
Getting Started
import { Pubky, PublicKey, Keypair, AuthFlowKind } from "@synonymdev/pubky";
// Initiate a Pubky SDK facade wired for default mainnet Pkarr relays.
const pubky = new Pubky(); // or: const pubky = Pubky.testnet(); for localhost testnet.
// 1) Create random user keys and bind to a new Signer.
const keypair = Keypair.random();
const signer = pubky.signer(keypair);
// 2) Sign up at a homeserver (optionally with an invite)
const homeserver = PublicKey.from(
"8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo"
);
const signupToken = "<your-invite-code-or-null>";
const session = await signer.signup(homeserver, signupToken);
// 3) Write a public JSON file (session-scoped storage uses cookies automatically)
const path = "/pub/example.com/hello.json";
await session.storage.putJson(path, { hello: "world" });
// 4) Read it publicly (no auth needed)
const userPk = session.info.publicKey.toString();
const addr = `${userPk}/pub/example.com/hello.json`;
const json = await pubky.publicStorage.getJson(addr); // -> { hello: "world" }
// 5) Authenticate on a 3rd-party app
const authFlow = pubky.startAuthFlow("/pub/my-cool-app/:rw", AuthFlowKind::signin()); // require permissions to read and write into `my.app`
renderQr(authFlow.authorizationUrl); // show to user
const session = await authFlow.awaitApproval();Find here ready-to-run examples.
Key formats (display vs transport)
PublicKey has two string forms:
- Display format:
pubky<z32>(for logs/UI/human-facing identifiers). - Transport/storage format: raw
z32(for hostnames, headers, query params, serde/JSON, DB storage).
Use publicKey.z32() for transport/storage. Use publicKey.toString() for display.
Initialization & events
The npm package bundles the WebAssembly module and initializes it before exposing any APIs. This avoids the common wasm-pack pitfall where events fire before the module finishes instantiating. Long-polling flows such as authFlow.awaitApproval() or authFlow.tryPollOnce() only start their relay calls after the underlying module is ready, so you won't miss approvals while the bundle is loading.
Reuse a single facade across your app
Use a shared Pubky (e.g, via context or prop drilling) instead of constructing one per request. This avoids reinitializing transports and keeps the same client available for repeated usage.
API Overview
Use new Pubky() to quickly get any flow started:
import { Pubky, Keypair, AuthFlowKind } from "@synonymdev/pubky";
// Mainnet (default relays)
const pubky = new Pubky();
// Local testnet wiring (Pkarr + HTTP mapping).
// Omit the argument for "localhost".
const pubkyLocal = Pubky.testnet("localhost");
// Signer (bind your keypair to a new Signer actor)
const signer = pubky.signer(Keypair.random());
// Pubky Auth flow (with capabilities)
const authFlow = pubky.startAuthFlow("/pub/my-cool-app/:rw", AuthFlowKind::signin());
// Public storage (read-only)
const publicStorage = pubky.publicStorage;
// Pkdns resolver
const pkdns = pubky.getHomeserverOf(publicKey);
// Optional: raw HTTP client for advanced use
const client = pubky.client;Client (HTTP bridge)
import { Client, PublicKey, resolvePubky } from "@synonymdev/pubky";
const client = new Client(); // or: pubky.client.fetch(); instead of constructing a client manually
// Convert the identifier into a transport URL before fetching.
const userId = PublicKey.from("pubky<z32>").toString();
const url = resolvePubky(`${userId}/pub/example.com/file.txt`);
const res = await client.fetch(url);Keys
import { Keypair, PublicKey } from "@synonymdev/pubky";
const keypair = Keypair.random();
const pubkey = keypair.publicKey;
// z-base-32 roundtrip
const parsed = PublicKey.from(pubkey.z32());
const displayId = pubkey.toString(); // pubky<z32> (display only)Recovery file (encrypt/decrypt root secret)
// Encrypt to recovery file (Uint8Array)
const recoveryFile = keypair.createRecoveryFile("strong passphrase");
// Decrypt back into a Keypair
const restored = Keypair.fromRecoveryFile(recoveryFile, "strong passphrase");
// Build a Signer from a recovered key
const signer = pubky.signer(restored);- keypair: An instance of Keypair.
- passphrase: A utf-8 string passphrase.
- Returns: A recovery file with a spec line and an encrypted secret key.
Signer & Session
import { Pubky, PublicKey, Keypair } from "@synonymdev/pubky";
const pubky = new Pubky();
const keypair = Keypair.random();
const signer = pubky.signer(keypair);
const homeserver = PublicKey.from("8pinxxgq…");
const session = await signer.signup(homeserver, /* invite */ null);
const session2 = await signer.signin(); // fast, prefer this; publishes PKDNS in background
const session3 = await signer.signinBlocking(); // slower but safer; waits for PKDNS publish
await session.signout(); // invalidates server sessionSession details
const userPk = session.info.publicKey.toString(); // -> pubky<z32> identifier
const caps = session.info.capabilities; // -> string[] permissions and paths
const storage = session.storage; // -> This User's storage API (absolute paths)Persist a session across tab refreshes (browser)
// Save the session snapshot (no secrets inside; relies on the HTTP-only cookie).
const snapshot = session.export();
localStorage.setItem("pubky-session", snapshot);
// Later (after a reload), rehydrate using the browser's stored cookie.
const restored = await pubky.restoreSession(localStorage.getItem("pubky-session")!);The exported string contains only public session metadata. The browser must keep the HTTP-only cookie alive for the restored session to remain authenticated.
Approve a pubkyauth request URL
await signer.approveAuthRequest("pubkyauth:///?caps=...&secret=...&relay=...");AuthFlow (pubkyauth)
End-to-end auth (3rd-party app asks a user to approve via QR/deeplink, E.g. Pubky Ring).
import { Pubky, AuthFlowKind } from "@synonymdev/pubky";
const pubky = new Pubky();
// Comma-separated capabilities string
const caps = "/pub/my-cool-app/:rw,/pub/another-app/folder/:w";
// Optional relay; defaults to Synonym-hosted relay if omitted
const relay = "https://httprelay.pubky.app/link/"; // optional (defaults to this)
// Start the auth polling
const flow = pubky.startAuthFlow(caps, AuthFlowKind::signin(), relay);
renderQr(flow.authorizationUrl); // show to user
// Blocks until the signer approves; returns a ready Session
const session = await flow.awaitApproval();Validate and normalize capabilities
If you accept capability strings from user input (forms, CLI arguments, etc.),
use validateCapabilities before calling startAuthFlow. The helper returns a
normalized string (ordering actions like :rw) and throws a structured error
when the input is malformed.
import { Pubky, validateCapabilities, AuthFlowKind } from "@synonymdev/pubky";
const pubky = new Pubky();
const rawCaps = formData.get("caps");
try {
const caps = validateCapabilities(rawCaps ?? "");
const flow = pubky.startAuthFlow(caps, AuthFlowKind::signin());
renderQr(flow.authorizationUrl);
const session = await flow.awaitApproval();
// ...
} catch (error) {
if (error.name === "InvalidInput") {
surfaceValidationError(error.message);
return;
}
throw error;
}On invalid input, validateCapabilities throws a PubkyError with
{ name: "InvalidInput", message: "Invalid capability entries: …" }, so you can
surface precise feedback to the user.
Http Relay & reliability
- If you don’t specify a relay,
PubkyAuthFlowdefaults to a Synonym-hosted relay. If that relay is down, logins won’t complete. - For production and larger apps, run your own http 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.
Storage
PublicStorage (read-only)
const pub = pubky.publicStorage;
// Reads
const response = await pub.get(
`${userPk}/pub/example.com/data.json`
); // -> Response (stream it)
await pub.getJson(`${userPk}/pub/example.com/data.json`);
await pub.getText(`${userPk}/pub/example.com/readme.txt`);
await pub.getBytes(`${userPk}/pub/example.com/icon.png`); // Uint8Array
// Metadata
await pub.exists(`${userPk}/pub/example.com/foo`); // boolean
await pub.stats(`${userPk}/pub/example.com/foo`); // { content_length, content_type, etag, last_modified } | null
// List directory (addressed path "<pubky>/pub/.../") must include trailing `/`.
// list(addr, cursor=null|suffix|fullUrl, reverse=false, limit?, shallow=false)
await pub.list(
`${userPk}/pub/example.com/`,
null,
false,
100,
false
);Use get() when you need the raw Response for streaming or custom parsing.
SessionStorage (read/write; uses cookies)
const s = session.storage;
// Writes
await s.putJson("/pub/example.com/data.json", { ok: true });
await s.putText("/pub/example.com/note.txt", "hello");
await s.putBytes("/pub/example.com/img.bin", new Uint8Array([1, 2, 3]));
// Reads
const response = await s.get("/pub/example.com/data.json"); // -> Response (stream it)
await s.getJson("/pub/example.com/data.json");
await s.getText("/pub/example.com/note.txt");
await s.getBytes("/pub/example.com/img.bin");
// Metadata
await s.exists("/pub/example.com/data.json");
await s.stats("/pub/example.com/data.json");
// Listing (session-scoped absolute dir)
await s.list("/pub/example.com/", null, false, 100, false);
// Delete
await s.delete("/pub/example.com/data.json");get() exposes the underlying Response, which is handy for streaming bodies or inspecting headers before consuming content.
Path rules:
- Session storage uses absolute paths like
"/pub/app/file.txt". - Public storage uses addressed form
pubky<user>/pub/app/file.txt(preferred) orpubky://<user>/....
Convention: put your app’s public data under a domain-like folder in /pub, e.g. /pub/my-new-app/.
PKDNS (Pkarr)
Resolve or publish _pubky records.
import { Pubky, PublicKey, Keypair } from "@synonymdev/pubky";
const pubky = new Pubky();
// Read-only resolver
const homeserver = await pubky.getHomeserverOf(PublicKey.from("pubky<z32>")); // string | undefined
// With keys (signer-bound)
const signer = pubky.signer(Keypair.random());
// Republish if missing or stale (reuses current host unless overridden)
await signer.pkdns.publishHomeserverIfStale();
// Or force an override now:
await signer.pkdns.publishHomeserverForce(/* optional override homeserver*/);
// Resolve your own homeserver:
await signer.pkdns.getHomeserver();Logging
The SDK ships with a WASM logger that bridges Rust log output into the browser or Node console. Call setLogLevel once at application start, before constructing Pubky or other SDK actors, to choose how verbose the logs should be.
import { setLogLevel } from "@synonymdev/pubky";
setLogLevel("debug"); // "error" | "warn" | "info" | "debug" | "trace"If the logger is already initialized, calling setLogLevel again will throw. Pick the most verbose level ("debug" or "trace") while developing to see pkarr resolution, network requests and storage operations in the console.
Resolve pubky identifiers into transport URLs
Use resolvePubky() when you need to feed an addressed resource into a raw HTTP client:
import { resolvePubky } from "@synonymdev/pubky";
const identifier =
"pubkyoperrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/pub/pubky.app/posts/0033X02JAN0SG";
const url = resolvePubky(identifier);
// -> "https://_pubky.operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/pub/pubky.app/posts/0033X02JAN0SG"Both pubky<pk>/… (preferred) and pubky://<pk>/… resolve to the same HTTPS endpoint.
WASM memory (free() helpers)
wasm-bindgen generates free() methods on exported classes (for example Pubky, AuthFlow PublicKey). JavaScript's GC eventually releases the underlying Rust structs on its own, but calling free() lets you drop them immediately if you are creating many short-lived instances (e.g. in a long-running worker). It is safe to skip manual frees in typical browser or Node apps.
Errors
All async methods throw a structured PubkyError:
interface PubkyError extends Error {
name:
| "RequestError" // network/server/validation/JSON
| "InvalidInput"
| "AuthenticationError"
| "PkarrError"
| "InternalError";
message: string;
data?: unknown; // structured context when available (e.g. { statusCode: number })
}Example:
try {
await publicStorage.getJson(`${pk}/pub/example.com/missing.json`);
} catch (e) {
const error = e as PubkyError;
if (
error.name === "RequestError" &&
typeof error.data === "object" &&
error.data !== null &&
"statusCode" in error.data &&
typeof (error.data as { statusCode?: number }).statusCode === "number" &&
(error.data as { statusCode?: number }).statusCode === 404
) {
// handle not found
}
}Browser environment notes
- Keep the Pubky client UI and the homeserver on the same origin family (both local or both remote). Browsers partition cookies by scheme/host, and cross-site requests (e.g., http://localhost calling https://staging…) can silently drop or cache
SameSite/Securesession cookies. - If you must mix environments, use a reverse proxy so the browser always talks to one consistent origin (or disable caching via devtools and clear cookies between switches).
- When troubleshooting auth/session caching: open a fresh incognito window, clear site data for the target origin, and verify the request includes credentials.
Local Test & Development
For test and development, you can run a local homeserver in a test network.
- Install Rust (for wasm and testnet builds):
curl https://sh.rustup.rs -sSf | sh- Install and run the local testnet:
cargo install pubky-testnet
pubky-testnet- Point the SDK at testnet:
import { Pubky } from "@synonymdev/pubky";
const pubky = Pubky.testnet(); // defaults to localhost
// or: const pubky = Pubky.testnet("testnet-host"); // custom host (e.g. Docker bridge)MIT © Synonym
