Getting started
Install @snapcap/native, construct a SnapcapClient with a DataStore + browser context, call authenticate(), and make your first request.
@snapcap/native is one TypeScript class. You hand it a DataStore, a BrowserContext (UA required), and — on first run — credentials. It gives you a small auth surface plus per-domain managers (client.friends, etc.). Everything else — the kameleon WASM boot, the WebLogin two-step, the SSO bearer mint, the cookie seeding, the bundle-native messaging session — is owned by the SDK.
Install
pnpm add @snapcap/native
# or
npm install @snapcap/nativeRequires Node 22+ or Bun 1.3+ — the bundle uses top-level await, the WASM imports rely on modern fetch, and the SDK depends on Node's vm.Context global-builtins behaviour (V8 fills the new realm with Object/Array/Promise/WebAssembly/...) that landed in those versions.
First call
import { SnapcapClient, FileDataStore } from "@snapcap/native";
const client = new SnapcapClient({
dataStore: new FileDataStore("./auth.json"),
credentials: {
username: process.env.SNAP_USER,
password: process.env.SNAP_PASS!,
},
browser: {
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36",
},
});
await client.authenticate();
const friends = await client.friends.list();
console.log(friends.map((f) => f.username));That is the entire pattern. The constructor does no I/O — it just constructs a per-instance Sandbox (vm.Context + happy-dom Window + shimmed I/O layer) wired to your DataStore. The first network call happens inside authenticate().
The constructor throws if browser.userAgent is missing. There's no shared default — every consumer defaulting to the same UA would itself become a snapcap fingerprint. Pass a recent realistic UA from a real browser; in multi-tenant deployments, vary it per tenant.
credentials are not persisted to the DataStore. Pass them again on every boot if you want automatic recovery from server-side session expiry.
Choosing a DataStore
Two implementations ship out of the box:
FileDataStore(path)— single-JSON-file persistence on disk. Survives process restarts. Good for development and single-tenant production.MemoryDataStore()— ephemeral in-process map. Cold-starts (full WebLogin + kameleon boot) every run. Good for tests and short-lived scripts.
For Redis / Postgres / KMS-wrapped persistence, implement the DataStore interface — see Persistence → Plugging in your own backend.
Clean shutdown
There's no client.dispose() to call. The per-instance Sandbox is GC-tracked; once the SnapcapClient reference is released and any messaging.on(...) subscriptions have been unsubscribed, the realm and all in-process state get collected.
If you want a clean network shutdown — closing the bundle's duplex WebSocket, tearing down the Zustand auth slice — call await client.logout(). It is safe to call even if the bundle session never came up.
A complete script
import { SnapcapClient, FileDataStore } from "@snapcap/native";
import { readFileSync } from "node:fs";
// `.snapcap-creds.json` is just `{"username":"…","password":"…"}` — local
// file, gitignored. Replace with your own credential-loading pattern in
// production.
const { username, password } = JSON.parse(readFileSync(".snapcap-creds.json", "utf8"));
const client = new SnapcapClient({
dataStore: new FileDataStore("./.tmp/auth/auth.json"),
credentials: { username, password },
browser: {
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36",
},
});
await client.authenticate();
// List the social graph.
const friends = await client.friends.list();
console.log(`${friends.length} mutual friends`);
// Search Snap's user index, then send a friend request to the first hit.
const hits = await client.friends.search("perdyjamie");
if (hits.length > 0) {
await client.friends.sendRequest(hits[0].userId);
console.log(`requested ${hits[0].username}`);
}
// Subscribe to real-time graph changes.
const unsub = client.friends.onChange((snap) => {
console.log(`mutuals=${snap.mutuals.length} received=${snap.received.length}`);
});
// ...later
unsub();Cold start vs warm start
Reuse the DataStore across processes (or workers, or PM restarts) to skip the kameleon boot + full WebLogin flow:
// Process 1 — fresh disk:
const a = new SnapcapClient({
dataStore: new FileDataStore("./auth.json"),
credentials: { username, password },
browser: { userAgent },
});
await a.authenticate(); // ~5 s — kameleon WASM + WebLoginService 2-step + SSO bearer mint
// Process 2 — same auth.json, credentials still required for refresh recovery:
const b = new SnapcapClient({
dataStore: new FileDataStore("./auth.json"),
credentials: { username, password },
browser: { userAgent },
});
await b.authenticate(); // ~100 ms — warm SSO redirect, bundle still loads- Cold start (
auth.jsonmissing or empty): kameleon WASM boots (~5 MB JS + 814 KB WASM),WebLoginServicetwo-step runs, SSO redirect mints a bearer, parent-domain cookies seed the jar. Persisted to the DataStore. Wall-clock around 5 seconds. - Warm start (
auth.jsonalready has valid cookies): the SSO redirect short-circuits in roughly one redirect, ~100 ms. The bundle still loads —authenticate()always brings up the chat bundle so the friend graph and Zustand auth slice are populated.
authenticate() also warms the friend-graph cache by calling client.friends.list() once before resolving. The fetch is best-effort — a friends sync failure is logged but never poisons the auth result — and seeds the persisted graph baseline so a later subscriber's first replay correctly identifies "new since last session" mutuals/requests instead of treating every existing entry as added.
If the bearer in the DataStore has expired, the next gRPC call gets a 401 and the SDK transparently mints a fresh bearer from the long-lived __Host-sc-a-auth-session cookie — no caller-side handling required. For a manual bearer-only refresh, call await client.refreshAuthToken().
Re-authenticate
There's no force flag. To re-run the login flow — say after a password rotation, or to recover from server-side session invalidation that didn't surface as a clean error — just call authenticate() again:
await client.authenticate();For a bearer-only refresh in place (cookies still valid), use refreshAuthToken() instead.
Logout
await client.logout();
// or, force the local teardown even if the server-side revoke fails:
await client.logout(true);logout() calls Snap's own state.auth.logout thunk to tear down the bundle's Zustand slice, then deletes cookie_jar from the DataStore. Bundle-owned local_* / session_* / indexdb_* entries — including the bundle-minted Fidelius wrapped identity at local_uds_uds.e2eeIdentityKey.shared — are intentionally left intact. Wiping them would force the next authenticate() to re-bootstrap WASM state from scratch, which costs an extra round-trip and ~250 ms. If you want a true wipe, drop the underlying DataStore.
Multi-account in one process
Each SnapcapClient owns its own Sandbox — its own vm.Context, its own happy-dom Window, its own shimmed I/O layer, its own bundle bring-up caches. Two clients in the same process do not share Zustand state, bearer tokens, or webpack runtime caches. They're isolated at the V8 vm.Context boundary.
In practice this means:
- One process can drive many accounts simultaneously.
- Each client needs its own
DataStore— they collide on the shared persistence keys otherwise. - Each client should use a different
browser.userAgentfor fingerprint diversity.
const a = new SnapcapClient({
dataStore: new FileDataStore("./auth/alice.json"),
credentials: { username: "alice", password: aPass },
browser: { userAgent: alicesUA },
});
const b = new SnapcapClient({
dataStore: new FileDataStore("./auth/bob.json"),
credentials: { username: "bob", password: bPass },
browser: { userAgent: bobsUA },
});
await Promise.all([a.authenticate(), b.authenticate()]);See Multi-tenant for the recommended layout, including shared throttle gates so aggregate request rate stays constant in N.
What's next
- Auth model — what
authenticate()actually does, the auth-state enum, refresh paths. - Persistence — the DataStore key layout and how to plug in a custom backend.
- Friends — graph reads, typed events, refresh cadence, offline replay.
- Messaging — live decrypted inbound, conversation listing, raw envelope backfill.
- Multi-tenant — running many accounts in one process.
- Throttling — opt-in HTTP rate limiting, per-instance and shared gates.
- Logging — structured network observability via
SNAP_NETLOG=1andsetLogger. - API reference — every public method.
- Internals — sandbox, shim layer, kameleon, SSO, bundle session.