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 Homeserver via HTTPS

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.

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 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 development, run a local Homeserver:

Terminal window
# Clone repository
git clone https://github.com/pubky/pubky-core
cd pubky-core
# Run testnet
cargo run --bin pubky-testnet

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