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.
Supported Platforms
Section titled “Supported Platforms”| Platform | Language | Status | Package |
|---|---|---|---|
| Rust | Rust | ✅ Stable | crates.io/crates/pubky |
| Web/Node | JavaScript/TypeScript | ✅ Stable | @synonymdev/pubky |
| React Native | JavaScript/TypeScript | ✅ Stable | @synonymdev/react-native-pubky |
| iOS | Swift | 🚧 Beta | Native bindings |
| Android | Kotlin | 🚧 Beta | Native bindings |
Installation
Section titled “Installation”cargo add pubkyJavaScript/TypeScript
Section titled “JavaScript/TypeScript”npm install @synonymdev/pubky# oryarn add @synonymdev/pubkyReact Native
Section titled “React Native”npm install @synonymdev/react-native-pubky# oryarn add @synonymdev/react-native-pubkyFor iOS, also run:
cd ios && pod installThe iOS SDK uses native Swift bindings generated via UniFFI. You can either:
Option 1: Use CocoaPods (Recommended)
pod 'PubkyCore'Option 2: Build from source
# Clone the FFI repositorygit clone https://github.com/pubky/pubky-core-fficd pubky-core-ffi./build.sh iosThe build generates:
bindings/ios/PubkyCore.xcframework- Native frameworkbindings/ios/pubkycore.swift- Swift bindings
See pubky-core-ffi for detailed integration instructions.
Android
Section titled “Android”The Android SDK uses native Kotlin bindings generated via UniFFI.
Build from source:
# Clone the FFI repositorygit clone https://github.com/pubky/pubky-core-fficd pubky-core-ffi./build.sh androidThe build generates:
bindings/android/jniLibs/- Native JNI libraries for all architecturesbindings/android/pubkycore.kt- Kotlin bindings
Copy these to your Android project:
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.
Core Concepts
Section titled “Core Concepts”Public-Key Identity
Section titled “Public-Key Identity”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
Homeserver Discovery
Section titled “Homeserver Discovery”The SDK uses PKARR to discover where a user’s data is hosted:
- Query Mainline DHT for public key
- Retrieve PKARR record with Homeserver URL
- Connect to Homeserver via HTTPS
Storage Paths
Section titled “Storage Paths”Data is organized in a hierarchical namespace:
/pub/app_name/path/to/data # Public, readable by anyone/private/app_name/secret # Private (future)API Reference
Section titled “API Reference”Client Creation
Section titled “Client Creation”Rust:
use pubky::Pubky;
let pubky = Pubky::new()?;JavaScript:
import { Pubky } from "@synonymdev/pubky";
const pubky = new Pubky();Sign Up (Create Account on Homeserver)
Section titled “Sign Up (Create Account on Homeserver)”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);Sign In (Existing User)
Section titled “Sign In (Existing User)”Rust:
let signer = pubky.signer(keypair);let session = signer.signin().await?;JavaScript:
const signer = pubky.signer(keypair);const session = await signer.signin();Sign In: Fast vs Blocking
Section titled “Sign In: Fast vs Blocking”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 backgroundlet 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 immediatelylet session = signer.signin_blocking().await?;JavaScript:
const signer = pubky.signer(keypair);
// Fast: PKDNS refresh happens in the backgroundconst 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 immediatelyconst sessionBlocking = await signer.signinBlocking();Store Data (PUT)
Section titled “Store Data (PUT)”Rust:
// Requires the "json" feature on the pubky cratesession .storage() .put_json("/pub/myapp/profile", &profile) .await?;JavaScript:
await session.storage.putJson("/pub/myapp/profile", profile);Retrieve Data (GET)
Section titled “Retrieve Data (GET)”Rust:
// Requires the "json" feature on the pubky cratelet profile: serde_json::Value = session.storage().get_json("/pub/myapp/profile").await?;JavaScript:
const profile = await session.storage.getJson("/pub/myapp/profile");Delete Data (DELETE)
Section titled “Delete Data (DELETE)”Rust:
session.storage().delete("/pub/myapp/profile").await?;JavaScript:
await session.storage.delete("/pub/myapp/profile");List Data (Pagination)
Section titled “List Data (Pagination)”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 Resource (Exists & Metadata)
Section titled “Check Resource (Exists & Metadata)”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 bodyif 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 storagelet 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 bodyconst 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 storageconst publicExists = await pubky.publicStorage.exists( `pubky://${userPk}/pub/myapp/profile` as Address,);Public Read (Unauthenticated)
Section titled “Public Read (Unauthenticated)”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,);Authentication Flows
Section titled “Authentication Flows”Third-Party Authorization
Section titled “Third-Party Authorization”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 scanlet session = flow.await_approval().await?;See Authentication for the full authentication flow.
React Native Usage
Section titled “React Native Usage”The React Native SDK (@synonymdev/react-native-pubky) provides the same API as the JavaScript SDK with mobile-optimized bindings built using UniFFI.
Basic Usage
Section titled “Basic Usage”import { signUp, signIn, put, get, list, deleteFile, generateSecretKey, getPublicKeyFromSecretKey,} from "@synonymdev/react-native-pubky";
// All methods return Result typeconst result = await signUp(secretKey, homeserverUrl);if (result.isErr()) { console.error(result.error.message);} else { console.log(result.value); // Success value}Sign Up & Authentication
Section titled “Sign Up & Authentication”import { signUp, signIn, signOut, revalidateSession, getHomeserver,} from "@synonymdev/react-native-pubky";
// Standard signupconst signUpRes = await signUp( secretKey, "pubky://8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo",);
// Signup with token (for gated homeservers)const signUpWithTokenRes = await signUp( secretKey, "pubky://8pinxxgqs41n4aididenw5apqp1urfmzdztr8jt4abrkdn435ewo", "your_signup_token",);
// Sign inconst signInRes = await signIn(secretKey);
// Get homeserverconst homeserverRes = await getHomeserver(publicKey);Data Operations
Section titled “Data Operations”import { put, get, list, deleteFile } from "@synonymdev/react-native-pubky";
// Write dataconst putRes = await put( "pubky://z4e8s17cou9qmuwen8p1556jzhf1wktmzo6ijsfnri9c4hnrdfty/pub/profile.json", { name: "Alice", bio: "Builder" }, secretKey,);
// Read dataconst getRes = await get( "pubky://z4e8s17cou9qmuwen8p1556jzhf1wktmzo6ijsfnri9c4hnrdfty/pub/profile.json",);
// List directoryconst listRes = await list( "pubky://z4e8s17cou9qmuwen8p1556jzhf1wktmzo6ijsfnri9c4hnrdfty/pub/posts/",);
// Delete fileconst deleteRes = await deleteFile( "pubky://z4e8s17cou9qmuwen8p1556jzhf1wktmzo6ijsfnri9c4hnrdfty/pub/old-post", secretKey,);Key Management
Section titled “Key Management”import { generateSecretKey, getPublicKeyFromSecretKey, createRecoveryFile, decryptRecoveryFile,} from "@synonymdev/react-native-pubky";
// Generate new key pairconst keyRes = await generateSecretKey();if (keyRes.isErr()) throw keyRes.error;const secretKey = keyRes.value.secret_key;
// Derive public keyconst pubKeyRes = await getPublicKeyFromSecretKey(secretKey);if (pubKeyRes.isErr()) throw pubKeyRes.error;const publicKey = pubKeyRes.value.public_key;
// Create encrypted recovery fileconst recoveryRes = await createRecoveryFile(secretKey, "passphrase");if (recoveryRes.isErr()) throw recoveryRes.error;const recoveryFile = recoveryRes.value; // Base64 encoded
// Decrypt recovery fileconst decryptRes = await decryptRecoveryFile(recoveryFile, "passphrase");if (decryptRes.isErr()) throw decryptRes.error;const recoveredKey = decryptRes.value;HTTPS Resolution
Section titled “HTTPS Resolution”import { resolveHttps } from "@synonymdev/react-native-pubky";
// Resolve public key to HTTPS URLconst resolveRes = await resolveHttps( "z4e8s17cou9qmuwen8p1556jzhf1wktmzo6ijsfnri9c4hnrdfty",);
if (resolveRes.isOk()) { console.log(`HTTPS records: ${JSON.stringify(resolveRes.value)}`);}Example: Complete Social Profile
Section titled “Example: Complete Social Profile”import { signUp, put, get } from "@synonymdev/react-native-pubky";
// Sign upconst 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 profileconst putRes = await put( "pubky://alice-pubkey/pub/pubky.app/profile.json", profile, secretKey,);
// Read profileconst getRes = await get("pubky://alice-pubkey/pub/pubky.app/profile.json");if (getRes.isErr()) throw getRes.error;const savedProfile = JSON.parse(getRes.value);Repository & Documentation
Section titled “Repository & Documentation”- NPM: @synonymdev/react-native-pubky
- GitHub: github.com/pubky/react-native-pubky
- Examples: Example App
Examples
Section titled “Examples”Simple Profile Storage
Section titled “Simple Profile Storage”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.
Social Feed Application
Section titled “Social Feed Application”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)}Complete Examples
Section titled “Complete Examples”The repository includes comprehensive examples:
JavaScript Examples:
- 0-logging.mjs - Setup and logging
- 1-testnet.mjs - Local testnet
- 2-signup.mjs - Identity creation
- 3-authenticator.mjs - Auth flow
- 4-storage.mjs - CRUD operations
- 5-request.mjs - Authorization
Rust Examples:
- 0-logging - Setup and logging
- 1-testnet - Local testnet
- 2-signup - Identity creation
- 3-auth_flow - Complete auth
- 4-storage - CRUD operations
- 5-request - Authorization
- 6-auth_flow_signup - Full signup flow
- 7-events_stream - SSE event streaming
Testing
Section titled “Testing”Local Testnet
Section titled “Local Testnet”For development, run a local Homeserver:
# Clone repositorygit clone https://github.com/pubky/pubky-corecd pubky-core
# Run testnetcargo run --bin pubky-testnetThen connect your app to http://localhost:15411.
JavaScript:
import { Pubky } from "@synonymdev/pubky";
const pubky = Pubky.testnet("http://localhost:15411");Unit Tests
Section titled “Unit Tests”JavaScript:
cd pubky-sdk/bindings/jsnpm run testnet # Start local servernpm test # Run testsRust:
cd pubky-sdkcargo testAdvanced Features
Section titled “Advanced Features”Event Streaming
Section titled “Event Streaming”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 withlive).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 subscriptionsEvent— A single event withevent_type,resource, andcursorEventCursor— Au64identifier used for resuming streams from a positionEventType— EitherPut(with Blake3content_hash) orDelete
See the 7-events_stream example for a complete CLI tool.
Session Management
Section titled “Session Management”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 sessionlet session = signer.signin().await?;
// Session infoprintln!("User: {}", session.info().public_key());
// Sign out invalidates the sessionsession.signout().await.map_err(|(e, _)| e)?;Session Persistence
Section titled “Session Persistence”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-authenticatingconst restored = await Session.restore(exported);Multiple Identities
Section titled “Multiple Identities”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 identityPlatform-Specific Notes
Section titled “Platform-Specific Notes”iOS Integration
Section titled “iOS Integration”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)Android Integration
Section titled “Android Integration”import pubky.PubkyClient
val client = PubkyClient()val keypair = client.signUp()println("Public Key: ${keypair.publicKey}")
client.put( path = "/pub/myapp/data", data = jsonData)Error Handling
Section titled “Error Handling”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; }}Best Practices
Section titled “Best Practices”-
Secure Key Storage: Never store private keys in plaintext
- iOS: Use Keychain Services
- Android: Use EncryptedSharedPreferences
- Web: Use secure storage APIs or Pubky Ring
-
Session Management: Use time-limited sessions, refresh regularly
-
Error Handling: Always handle network errors and retries
-
Rate Limiting: Respect Homeserver rate limits
-
Data Validation: Validate data before storing and after retrieving
-
Namespacing: Use consistent path structures per application
Resources
Section titled “Resources”- Rust API Docs: docs.rs/pubky
- Repository: github.com/pubky/pubky-core
- NPM Package: @synonymdev/pubky
- React Native Package: @synonymdev/react-native-pubky
- React Native Repository: github.com/pubky/react-native-pubky
- iOS/Android FFI: github.com/pubky/pubky-core-ffi - Native bindings via UniFFI
- Examples: github.com/pubky/pubky-core/tree/main/examples
- Pubky Core Overview: Main documentation
- API Reference: HTTP API specification
The Pubky SDK makes it easy to build decentralized applications with standard web technologies.