Skip to content

Pubky SDK: Client Libraries for Decentralized Applications

The Pubky SDK provides client libraries for building applications on Pubky Core. Available in multiple languages with consistent APIs across platforms.

PlatformLanguageStatusPackage
RustRust✅ Stablecrates.io/crates/pubky
Web/NodeJavaScript/TypeScript✅ Stable@synonymdev/pubky
React NativeJavaScript/TypeScript✅ Stable@synonymdev/react-native-pubky
iOSSwift🚧 BetaNative bindings
AndroidKotlin🚧 BetaNative bindings
Terminal window
cargo add pubky
Terminal window
npm install @synonymdev/pubky
# or
yarn add @synonymdev/pubky
Terminal window
npm install @synonymdev/react-native-pubky
# or
yarn add @synonymdev/react-native-pubky

For iOS, also run:

Terminal window
cd ios && pod install

The iOS SDK uses native Swift bindings generated via UniFFI. You can either:

Option 1: Use CocoaPods (Recommended)

pod 'PubkyCore'

Option 2: Build from source

Terminal window
# Clone the FFI repository
git clone https://github.com/pubky/pubky-core-ffi
cd pubky-core-ffi
./build.sh ios

The build generates:

  • bindings/ios/PubkyCore.xcframework - Native framework
  • bindings/ios/pubkycore.swift - Swift bindings

See pubky-core-ffi for detailed integration instructions.

The Android SDK uses native Kotlin bindings generated via UniFFI.

Build from source:

Terminal window
# Clone the FFI repository
git clone https://github.com/pubky/pubky-core-ffi
cd pubky-core-ffi
./build.sh android

The build generates:

  • bindings/android/jniLibs/ - Native JNI libraries for all architectures
  • bindings/android/pubkycore.kt - Kotlin bindings

Copy these to your Android project:

Terminal window
cp -r bindings/android/jniLibs/* app/src/main/jniLibs/
cp bindings/android/pubkycore.kt app/src/main/java/

See pubky-core-ffi for detailed integration instructions.

Every user is identified by an Ed25519 public key:

  • 32-byte public key (encoded as z-base-32)
  • Corresponds to a private key held securely by the user
  • Forms the basis of authentication and data ownership

The SDK uses PKARR to discover where a user’s data is hosted:

  1. Query Mainline DHT for public key
  2. Retrieve PKARR record with Homeserver URL
  3. Connect to the Homeserver via HTTPS or PubkyTLS

Use SDK methods for ordinary storage and event operations instead of constructing Homeserver URLs yourself. For signed-in storage, pass the path, such as /pub/myapp/profile; the session supplies the user. For public reads or event subscriptions, pass the user’s public key plus the path or event filter. The SDK turns that into the right HTTP request: it resolves the user’s PKARR record, chooses PubkyTLS or standard HTTPS for the runtime, attaches session cookies when needed, and adds pubky-host when an HTTPS Homeserver endpoint needs the target public key. Choose endpoints or set pubky-host yourself only when you intentionally use the raw Homeserver HTTP API.

Data is organized in a hierarchical namespace:

/pub/app_name/path/to/data # Public, readable by anyone
/private/app_name/secret # Private (future)

Rust:

use pubky::Pubky;
let pubky = Pubky::new()?;

JavaScript:

import { Pubky } from "@synonymdev/pubky";
const pubky = new Pubky();

For gated homeservers, obtain a signup token via Homegate first. Pass None/null only for open homeservers or local testnets.

Rust:

use pubky::{Keypair, Pubky, PublicKey};
let pubky = Pubky::new()?;
let keypair = Keypair::random();
let homeserver =
PublicKey::try_from("8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo").unwrap();
let signer = pubky.signer(keypair);
let session = signer.signup(&homeserver, signup_token.as_deref()).await?;

JavaScript:

const signer = pubky.signer(keypair);
const session = await signer.signup(homeserverPk, signupToken);

Rust:

let signer = pubky.signer(keypair);
let session = signer.signin().await?;

JavaScript:

const signer = pubky.signer(keypair);
const session = await signer.signin();

signin() returns quickly by refreshing PKDNS in the background. signinBlocking() waits until the user’s homeserver is discoverable via PKDNS (~3-5s), which is useful when you need immediate resolvability after sign-in:

Rust:

use pubky::{Keypair, Pubky};
let pubky = Pubky::new()?;
let signer = pubky.signer(Keypair::random());
// Fast: PKDNS refresh happens in the background
let session = signer.signin().await?;
// Blocking: waits for PKDNS to be discoverable (~3-5s)
// Use this when you need the user's homeserver to be resolvable immediately
let session = signer.signin_blocking().await?;

JavaScript:

const signer = pubky.signer(keypair);
// Fast: PKDNS refresh happens in the background
const session = await signer.signin();
// Blocking: waits for PKDNS to be discoverable (~3-5s)
// Use this when you need the user's homeserver to be resolvable immediately
const sessionBlocking = await signer.signinBlocking();

Rust:

// Requires the "json" feature on the pubky crate
session
.storage()
.put_json("/pub/myapp/profile", &profile)
.await?;

JavaScript:

await session.storage.putJson("/pub/myapp/profile", profile);

Rust:

// Requires the "json" feature on the pubky crate
let profile: serde_json::Value = session.storage().get_json("/pub/myapp/profile").await?;

JavaScript:

const profile = await session.storage.getJson("/pub/myapp/profile");

Rust:

session.storage().delete("/pub/myapp/profile").await?;

JavaScript:

await session.storage.delete("/pub/myapp/profile");

Rust:

let entries = session
.storage()
.list("/pub/myapp/posts/")?
.limit(20)
.reverse(true)
.send()
.await?;
for entry in entries {
println!("{}", entry);
}

JavaScript:

const entries = await session.storage.list(
"/pub/myapp/posts/",
null,
false,
20,
);
for (const url of entries) {
console.log(url);
}

Check if data at a given storage path exists, or retrieve its metadata (size, MIME type, ETag for cache validation) without downloading the body:

Rust:

// Check if a resource exists (lightweight HEAD request)
let exists = session.storage().exists("/pub/myapp/profile").await?;
// Get resource metadata without downloading the body
if let Some(stats) = session.storage().stats("/pub/myapp/profile").await? {
println!("Size: {:?}", stats.content_length);
println!("Type: {:?}", stats.content_type);
println!("ETag: {:?}", stats.etag);
}
// Also available on public storage
let user = PublicKey::try_from(user_public_key).unwrap();
let public_exists = pubky
.public_storage()
.exists((&user, "/pub/myapp/profile"))
.await?;

JavaScript:

// Check if a resource exists (lightweight HEAD request)
const exists = await session.storage.exists("/pub/myapp/profile");
// Get resource metadata without downloading the body
const stats = await session.storage.stats("/pub/myapp/profile");
if (stats) {
console.log("Size:", stats.contentLength);
console.log("Type:", stats.contentType);
console.log("ETag:", stats.etag);
}
// Also available on public storage
const publicExists = await pubky.publicStorage.exists(
`pubky://${userPk}/pub/myapp/profile` as Address,
);

Read another user’s public data without a session:

Rust:

let user = PublicKey::try_from(user_public_key).unwrap();
let resp = pubky
.public_storage()
.get((&user, "/pub/myapp/profile"))
.await?;
let text = resp.text().await?;

JavaScript:

const text = await pubky.publicStorage.getText(
`pubky://${userPk}/pub/myapp/profile` as Address,
);

Pubky Core supports OAuth-style authorization for third-party apps via the pubkyauth:// protocol:

use pubky::{AuthFlowKind, Capabilities, Pubky};
let pubky = Pubky::new()?;
let caps = Capabilities::default();
let flow = pubky.start_auth_flow(&caps, AuthFlowKind::signin())?;
// Display flow.authorization_url() as QR code for Pubky Ring to scan
let session = flow.await_approval().await?;

See Authentication for the full authentication flow.

Resuming is mainly for in-progress auth requests where the app loses the original flow object before the user approves, such as a page refresh, route reload, tab recovery, or native app restart. Save the original authorizationUrl when the flow starts, then pass it back to the SDK to reconnect to the same relay inbox instead of asking the user to scan a new request.

This only works while the relay channel is still within its retention window. Store the URL only in short-lived storage such as sessionStorage; it contains the client_secret, so delete it after approval or when the flow is abandoned.

Rust:

use pubky::{AuthFlowKind, Capabilities, Pubky};
let pubky = Pubky::new()?;
let caps = Capabilities::default();
let flow = pubky.start_auth_flow(&caps, AuthFlowKind::signin())?;
// Persist only for the short relay TTL; the URL contains a client secret.
let authorization_url = flow.authorization_url().to_string();
// After restart or refresh, reconnect to the same relay channel.
let resumed = pubky.resume_auth_flow(&authorization_url)?;
let session = resumed.await_approval().await?;

JavaScript:

const flow = pubky.startAuthFlow("/pub/myapp/:rw", AuthFlowKind.signin());
// Store only for the short relay TTL; authorizationUrl contains a secret.
sessionStorage.setItem("pubky-auth-url", flow.authorizationUrl);
// After a refresh, reconnect to the same relay channel.
const saved = sessionStorage.getItem("pubky-auth-url");
const resumed = saved ? pubky.resumeAuthFlow(saved) : flow;
const session = await resumed.awaitApproval();
sessionStorage.removeItem("pubky-auth-url");

The React Native SDK (@synonymdev/react-native-pubky) provides the same API as the JavaScript SDK with mobile-optimized bindings built using UniFFI.

import {
signUp,
signIn,
put,
get,
list,
deleteFile,
generateSecretKey,
getPublicKeyFromSecretKey,
} from "@synonymdev/react-native-pubky";
// All methods return Result type
const result = await signUp(secretKey, homeserverUrl);
if (result.isErr()) {
console.error(result.error.message);
} else {
console.log(result.value); // Success value
}
import {
signUp,
signIn,
signOut,
revalidateSession,
getHomeserver,
} from "@synonymdev/react-native-pubky";
// Standard signup
const signUpRes = await signUp(
secretKey,
"pubky://8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo",
);
// Signup with token (for gated homeservers)
const signUpWithTokenRes = await signUp(
secretKey,
"pubky://8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo",
"your_signup_token",
);
// Sign in
const signInRes = await signIn(secretKey);
// Get homeserver
const homeserverRes = await getHomeserver(publicKey);
import { put, get, list, deleteFile } from "@synonymdev/react-native-pubky";
// Write data
const putRes = await put(
"pubky://z4e8s17cou9qmuwen8p1556jzhf1wktmzo6ijsfnri9c4hnrdfty/pub/profile.json",
{ name: "Alice", bio: "Builder" },
secretKey,
);
// Read data
const getRes = await get(
"pubky://z4e8s17cou9qmuwen8p1556jzhf1wktmzo6ijsfnri9c4hnrdfty/pub/profile.json",
);
// List directory
const listRes = await list(
"pubky://z4e8s17cou9qmuwen8p1556jzhf1wktmzo6ijsfnri9c4hnrdfty/pub/posts/",
);
// Delete file
const deleteRes = await deleteFile(
"pubky://z4e8s17cou9qmuwen8p1556jzhf1wktmzo6ijsfnri9c4hnrdfty/pub/old-post",
secretKey,
);
import {
generateSecretKey,
getPublicKeyFromSecretKey,
createRecoveryFile,
decryptRecoveryFile,
} from "@synonymdev/react-native-pubky";
// Generate new key pair
const keyRes = await generateSecretKey();
if (keyRes.isErr()) throw keyRes.error;
const secretKey = keyRes.value.secret_key;
// Derive public key
const pubKeyRes = await getPublicKeyFromSecretKey(secretKey);
if (pubKeyRes.isErr()) throw pubKeyRes.error;
const publicKey = pubKeyRes.value.public_key;
// Create encrypted recovery file
const recoveryRes = await createRecoveryFile(secretKey, "passphrase");
if (recoveryRes.isErr()) throw recoveryRes.error;
const recoveryFile = recoveryRes.value; // Base64 encoded
// Decrypt recovery file
const decryptRes = await decryptRecoveryFile(recoveryFile, "passphrase");
if (decryptRes.isErr()) throw decryptRes.error;
const recoveredKey = decryptRes.value;
import { resolveHttps } from "@synonymdev/react-native-pubky";
// Resolve public key to HTTPS URL
const resolveRes = await resolveHttps(
"z4e8s17cou9qmuwen8p1556jzhf1wktmzo6ijsfnri9c4hnrdfty",
);
if (resolveRes.isOk()) {
console.log(`HTTPS records: ${JSON.stringify(resolveRes.value)}`);
}
import { signUp, put, get } from "@synonymdev/react-native-pubky";
// Sign up
const signUpRes = await signUp(secretKey, homeserverUrl);
if (signUpRes.isErr()) throw new Error(signUpRes.error.message);
// Create profile (following pubky-app-specs)
const profile = {
name: "Alice",
bio: "Building on Pubky",
image: "pubky://alice-pubkey/pub/profile.jpg",
links: [{ title: "Website", url: "https://alice.com" }],
};
// Write profile
const putRes = await put(
"pubky://alice-pubkey/pub/pubky.app/profile.json",
profile,
secretKey,
);
// Read profile
const getRes = await get("pubky://alice-pubkey/pub/pubky.app/profile.json");
if (getRes.isErr()) throw getRes.error;
const savedProfile = JSON.parse(getRes.value);
import { Pubky, Keypair } from "@synonymdev/pubky";
async function storeProfile() {
const pubky = new Pubky();
const keypair = Keypair.random();
const signer = pubky.signer(keypair);
// Sign up at a homeserver (null token for open/testnet homeservers)
const session = await signer.signup(homeserverPk, signupToken);
console.log(`Public Key: ${signer.publicKey.z32()}`);
// Store profile (following pubky-app-specs format)
const profile = {
name: "Alice",
bio: "Building on Pubky",
image: "pubky://user_id/pub/pubky.app/files/0000000000000",
links: [{ title: "GitHub", url: "https://github.com/alice" }],
status: "Exploring decentralized tech.",
};
// Store at standard pubky-app location
await session.storage.putJson("/pub/pubky.app/profile.json", profile);
console.log("Profile stored!");
// Retrieve profile
const retrieved = await session.storage.getJson(
"/pub/pubky.app/profile.json",
);
console.log("Retrieved:", retrieved);
}

Note: This example follows the pubky-app-specs data model specification for interoperability with the pubky.app ecosystem.

use pubky::{Keypair, Pubky, PubkyResource, PubkySession, PublicKey};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Post {
content: String,
timestamp: i64,
author: String,
}
async fn publish_post(session: &PubkySession, post: &Post) -> anyhow::Result<()> {
let post_id = post.timestamp.to_string();
let path = format!("/pub/social/posts/{}", post_id);
// Requires the "json" feature on the pubky crate
session.storage().put_json(&path, post).await?;
Ok(())
}
async fn get_feed(pubky: &Pubky, public_key: &PublicKey) -> anyhow::Result<Vec<Post>> {
let entries: Vec<PubkyResource> = pubky
.public_storage()
.list((public_key, "/pub/social/posts/"))?
.limit(50)
.reverse(true)
.send()
.await?;
let mut posts = Vec::new();
for entry in entries {
// Requires the "json" feature on the pubky crate
let post: Post = pubky.public_storage().get_json(&entry).await?;
posts.push(post);
}
Ok(posts)
}

The repository includes comprehensive examples:

JavaScript Examples:

Rust Examples:

For browser or JavaScript development, use the fixed-port local testnet from Pubky Core. The Pubky Testnet README keeps the current setup steps for Docker-managed PostgreSQL, external PostgreSQL, and in-process Rust testnets.

After it is running, connect your app to http://localhost:15411.

JavaScript:

import { Pubky } from "@synonymdev/pubky";
const pubky = Pubky.testnet("http://localhost:15411");

JavaScript:

Terminal window
cd pubky-sdk/bindings/js
npm run testnet # Start local server
npm test # Run tests

Rust:

Terminal window
cd pubky-sdk
cargo test

The SDK provides a builder API for subscribing to real-time homeserver events via SSE. See Event Streaming for the underlying HTTP endpoint.

Rust — Single user:

use futures_util::StreamExt;
use pubky::{EventType, Pubky, PublicKey};
let pubky = Pubky::new()?;
let user = PublicKey::try_from("o1gg96ewuojmopcjbz8895478wdtxtzzuxnfjjz8o8e77csa1ngo").unwrap();
let mut stream = pubky
.event_stream_for_user(&user, None)
.live()
.subscribe()
.await?;
while let Some(result) = stream.next().await {
let event = result?;
println!(
"{}: {} (cursor: {})",
event.event_type, event.resource, event.cursor
);
}

Rust — Multiple users on the same homeserver:

use futures_util::StreamExt;
use pubky::{EventCursor, Pubky, PublicKey};
let pubky = Pubky::new()?;
let user1 =
PublicKey::try_from("o1gg96ewuojmopcjbz8895478wdtxtzzuxnfjjz8o8e77csa1ngo").unwrap();
let user2 =
PublicKey::try_from("pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy").unwrap();
let homeserver = pubky.get_homeserver_of(&user1).await.unwrap();
let mut stream = pubky
.event_stream_for(&homeserver)
.add_users([(&user1, None), (&user2, Some(EventCursor::new(100)))])?
.live()
.limit(100)
.path("/pub/")
.subscribe()
.await?;
while let Some(result) = stream.next().await {
let event = result?;
println!("{}: {}", event.event_type, event.resource);
}

JavaScript:

const user = PublicKey.from(
"o1gg96ewuojmopcjbz8895478wdtxtzzuxnfjjz8o8e77csa1ngo",
);
const stream = await pubky.eventStreamForUser(user, null).live().subscribe();
for await (const event of stream) {
console.log(`${event.eventType}: ${event.resource.path}`);
// event.eventType: "PUT" or "DEL"
// event.cursor: string (for pagination/resumption)
// event.contentHash: base64 string (PUT only) or undefined
}

Builder options:

  • .live() — After historical events, keep streaming new events in real-time
  • .reverse() — Deliver events newest-first (cannot combine with live)
  • .limit(n) — Maximum events to receive before closing
  • .path("/pub/...") — Filter events by path prefix
  • .add_users([(pubkey, cursor), ...]) — Subscribe to multiple users (up to 50)

Key types:

  • EventStreamBuilder — Fluent builder for configuring subscriptions
  • Event — A single event with event_type, resource, and cursor
  • EventCursor — A u64 identifier used for resuming streams from a position
  • EventType — Either Put (with Blake3 content_hash) or Delete

See the 7-events_stream example for a complete CLI tool.

Sessions are created via the Signer and provide scoped storage access:

use pubky::{Keypair, Pubky};
let pubky = Pubky::new()?;
let signer = pubky.signer(Keypair::random());
// Sign in returns a session
let session = signer.signin().await?;
// Session info
println!("User: {}", session.info().public_key());
// Sign out invalidates the session
session.signout().await.map_err(|(e, _)| e)?;

Export a session to a portable string (e.g. save to disk) so it survives process restarts. On restart, call import_secret to restore the session without repeating the full auth flow. If available, pass an existing client to reuse its connection pool instead of creating a new one:

Rust:

// Export session as a portable string (e.g. save to disk before shutdown)
let token = session.export_secret();
// On restart, restore without re-authenticating.
// Pass the existing client to reuse its connection pool.
let restored = pubky::PubkySession::import_secret(&token, Some(pubky.client().clone())).await?;

JavaScript:

// Export session as a portable string (e.g. save to storage before shutdown)
const exported = session.export();
// On restart, restore without re-authenticating
const restored = await Session.restore(exported);
let pubky = Pubky::new()?;
let session1 = pubky.signer(keypair_1).signin().await?;
let session2 = pubky.signer(keypair_2).signin().await?;
// Each session maintains a separate identity
import PubkySDK
let client = PubkyClient()
let keypair = try await client.signUp()
print("Public Key: \(keypair.publicKey)")
try await client.put(
path: "/pub/myapp/data",
data: jsonData
)
import pubky.PubkyClient
val client = PubkyClient()
val keypair = client.signUp()
println("Public Key: ${keypair.publicKey}")
client.put(
path = "/pub/myapp/data",
data = jsonData
)

Rust:

use pubky::{Error, errors::RequestError};
match session.storage().get("/pub/myapp/data").await {
Ok(resp) => println!("Retrieved: {}", resp.text().await?),
Err(Error::Request(RequestError::Server { status, message })) => {
eprintln!("Server error {status}: {message}");
}
Err(Error::Request(e)) => eprintln!("Request failed: {e}"),
Err(Error::Pkarr(e)) => eprintln!("PKARR error: {e}"),
Err(Error::Parse(e)) => eprintln!("URL parse error: {e}"),
Err(Error::Authentication(e)) => eprintln!("Auth failed: {e}"),
Err(Error::Build(e)) => eprintln!("Client build failed: {e}"),
}

JavaScript:

try {
const text = await session.storage.getText("/pub/myapp/data");
console.log("Retrieved:", text);
} catch (e) {
const error = e as import("@synonymdev/pubky").PubkyError;
switch (error.name) {
case "RequestError":
console.error("Network or server error:", error.message);
break;
case "InvalidInput":
console.error("Invalid input:", error.message);
break;
case "AuthenticationError":
console.error("Authentication failed:", error.message);
break;
case "PkarrError":
console.error("PKARR resolution failed:", error.message);
break;
case "ClientStateError":
console.error("Client state error:", error.message);
break;
case "InternalError":
console.error("Internal SDK error:", error.message);
break;
}
}
  1. Secure Key Storage: Never store private keys in plaintext

    • iOS: Use Keychain Services
    • Android: Use EncryptedSharedPreferences
    • Web: Use secure storage APIs or Pubky Ring
  2. Session Management: Use time-limited sessions, refresh regularly

  3. Error Handling: Always handle network errors and retries

  4. Rate Limiting: Respect Homeserver rate limits

  5. Data Validation: Validate data before storing and after retrieving

  6. Namespacing: Use consistent path structures per application


The Pubky SDK makes it easy to build decentralized applications with standard web technologies.