What are you trying to do?
Add login to my React Native app
Passwordless authentication with passkeys and OTP via a native Identity dialog.
Upload and download encrypted files
Resumable file transfer scaled for any size with a single canonical path.
Manage encrypted vaults
CRUD operations, unseal/seal, sharing config, and access logs.
Installation
npm install @sparkvault/sdk-mobile
The SDK is host-agnostic by design: you inject platform adapters for HTTP transport, secure token storage, file IO, and (optionally) the native Identity dialog. See Expo setup below for a complete Expo configuration.
Only consumers that render the <SparkVaultIdentityDialog /> component need React Native at runtime. Other consumers can import from @sparkvault/sdk-mobile directly without pulling React in.
Modules
The mobile SDK mirrors the web SDK's module model for React Native apps and ships a native Identity dialog driven by the same Identity Product configuration as the web popup.
| Module | Purpose |
|---|---|
SparkVaultIdentityDialog | Native Identity popup driven by Identity Product config, branding, and enabled methods |
client.products.identity | Identity Product config, OTP/passkey login, JWKS/JWT verification, and second-factor submission |
client.account | First-party Core session: Identity-token exchange, signup completion, and profile/account |
client.vaults | Vault CRUD, unseal/seal, sharing, access-log retention, and upload portal/widget settings |
client.folders | Folder CRUD, breadcrumbs, folder moves, and ingot moves |
client.ingots | List/search/get, upload/replace/rename/delete, sharing, access logs, downloads |
client.health | Root API availability checks |
client.sparks | SparkSync ephemeral secrets: list/get/create/delete and SparkLink sharing (share/getShare/unshare) |
client.entropy | HSM-backed entropy generation |
client.notify | Metadata-only notifications inbox (getInbox, getUnreadCount, markState) plus Expo device push registration (registerDevice, unregisterDevice) |
client.billing | Subscription and usage, Stripe customer portal, and native in-app purchase validation |
client.users | Account member management: list, invite, remove, resend invite |
The client also exposes client.onAuthEvent(listener): subscribe to auth/session events ('logout', 'token_refreshed') to react to token refresh failures (e.g. sign the user out). It returns an unsubscribe function.
Expo Setup
Pass the platform adapters the SDK needs into createSparkVaultMobileClient. The SDK validates each adapter and fails fast at construction if a required one is missing.
import * as FileSystem from 'expo-file-system/legacy';
import { createSparkVaultMobileClient } from '@sparkvault/sdk-mobile';
const client = createSparkVaultMobileClient({
tokenStorage, // your SecureStore-backed adapter
identityAccountId: 'acc_YOUR_ACCOUNT_ID',
userAgent: 'YourApp/1.0.0 (Mobile)',
fetch: xhrFetch, // your fetch wrapper
fileReader: {
readAsBase64: (fileUri, { position, length }) =>
FileSystem.readAsStringAsync(fileUri, {
encoding: 'base64',
position,
length,
}),
},
fileDownloader: {
download: async (url, fileUri, options) => {
const resumable = FileSystem.createDownloadResumable(
url,
fileUri,
{},
progress => options?.onProgress?.({
bytesWritten: progress.totalBytesWritten,
bytesTotal: progress.totalBytesExpectedToWrite || undefined,
})
);
const result = await resumable.downloadAsync();
return result ? { uri: result.uri, status: result.status, headers: result.headers } : null;
},
getInfo: async (fileUri) => {
const info = await FileSystem.getInfoAsync(fileUri);
return { exists: info.exists, sizeBytes: info.exists ? info.size : undefined };
},
delete: (fileUri) => FileSystem.deleteAsync(fileUri, { idempotent: true }),
},
});
tokenStorageandidentityAccountIdare required: construction throws if missing (identityAccountIdmust start withacc_).fetchfalls back toglobalThis.fetch.fileReaderis required foringots.uploadFromUri.fileDownloaderis required foringots.downloadToFile.loggeris optional: defaults to a no-op logger.passkeyProvideris optional and passed to the Identity dialog, not the client. When absent, the passkey method is hidden from the dialog.- Production downloader adapters must honor
abortSignal+timeoutMs, and throw on transport failure rather than writing an error response as a successful file.
Optional config fields and their defaults:
| Field | Default |
|---|---|
apiBaseUrl | https://api.sparkvault.com/v1 |
identityBaseUrl | https://auth.sparkvault.com |
preloadIdentityConfig | true: prefetches the Identity Product config at construction |
timeoutMs | 30000: general request timeout |
fileTransferTimeoutMs | 300000: file download timeout |
tusPostTimeoutMs | 30000: tus upload-creation timeout |
tusChunkTimeoutMs | 120000: per-chunk upload timeout |
tusChunkSizeBytes | 5242880 (5 MiB) |
allowedDownloadHostPatterns | SparkVault platform domains plus *.amazonaws.com / *.cloudfront.net: signed download URLs are validated against this allowlist |
Use Case: Native Identity Login
Render <SparkVaultIdentityDialog /> to handle passkey or OTP verification. The dialog applies your Identity Product branding/theme and returns the raw signed JWT plus JWKS for local verification.
The mobile SDK default Identity base URL is https://auth.sparkvault.com.
JWT verification enforces issuer https://auth.sparkvault.com/{account_id}
and audience {account_id}.
import * as Passkey from 'react-native-passkeys';
import {
SparkVaultIdentityDialog,
type MobilePasskeyProvider,
} from '@sparkvault/sdk-mobile/identity-dialog';
const passkeyProvider: MobilePasskeyProvider = {
isSupported: () => Passkey.isSupported(),
authenticate: async (options) => {
const credential = await Passkey.get({
challenge: options.challenge,
timeout: options.timeout,
rpId: options.rpId,
userVerification: options.userVerification,
});
return credential
? {
id: credential.id,
rawId: credential.rawId,
type: 'public-key',
response: {
clientDataJSON: credential.response.clientDataJSON,
authenticatorData: credential.response.authenticatorData,
signature: credential.response.signature,
userHandle: credential.response.userHandle,
},
}
: null;
},
};
<SparkVaultIdentityDialog
client={client}
visible={visible}
passkeyProvider={passkeyProvider}
onCancel={() => setVisible(false)}
onSuccess={async (result) => {
await client.products.identity.verifyIdentityToken(result.token, { jwks: result.jwks });
const session = await client.account.exchangeIdentityToken(result.token);
if ('needs_signup_info' in session) {
await client.account.completeSignup(session.signup_token, {
organization_name: 'Acme',
});
}
}}
/>
The dialog fetches /config, derives email-only, phone-only, or email-or-phone input from enabled delivery methods, applies branding/theme, and offers enabled mobile-supported methods (passkey, otp_email, otp_sms, otp_voice). Passkey authentication is discoverable and routes by the SVID in the WebAuthn user handle. Your provider must return response.userHandle from the assertion, or the Identity server cannot resolve the account and the login fails. It returns the raw Identity JWT and JWKS metadata so your app can verify before continuing.
Optional dialog props: initialIdentity?: string and initialIdentityType?: 'email' | 'phone' pre-fill the identity input, and onError?(error) surfaces dialog errors to your app.
Always verify the JWT before trusting it. Call client.products.identity.verifyIdentityToken(token, { jwks }) before account.exchangeIdentityToken. The SDK pins the signature algorithm to EdDSA, verifies the signature with cached JWKS, and enforces iss/aud/exp/nbf/iat claims.
exchangeIdentityToken() creates SparkVault Core mobile sessions. Apps using Identity as
their own login provider should verify the Identity JWT on their backend, key product records by
svid, and either issue their own native app session or use OIDC authorization code + PKCE.
Native refresh tokens belong in OS secure storage; the browser BFF cookie model applies only to web clients.
Use Case: Upload, Download, Replace
All transfer flows go through a single canonical path. Upload uses Forge with the tus v1.0.0 resumable protocol; download streams to a file URI via your fileDownloader adapter with automatic retry on transient failures. There are no size-based forks: every transfer scales from 1 byte to 5 TB.
const session = await client.vaults.unseal(vaultId, { vmk });
// Upload from a local file URI
await client.ingots.uploadFromUri({
vaultId,
vat: session.vat,
fileUri,
fileSize,
name: 'photo.jpg',
contentType: 'image/jpeg',
folder: 'Trips/2026', // optional vault path; missing segments are find-or-created
onProgress: (uploaded, total) => console.log(uploaded / total),
});
// Replace an existing ingot's data
await client.ingots.replaceFromUri({
vaultId,
ingotId,
vat: session.vat,
fileUri,
fileSize,
name: 'photo.jpg',
contentType: 'image/jpeg',
});
// Download to a file URI: single OS-streaming path, retries with fresh URLs
const result = await client.ingots.downloadToFile({
vaultId,
ingotId,
vat: session.vat,
fileUri: `${FileSystem.cacheDirectory}photo.jpg`,
expectedSizeBytes,
onProgress: (written, total) => console.log(written / total),
});
The lower-level primitives behind these flows are also exported: ingots.createUpload(options) initializes the ingot record and returns the Forge resumable-upload URL with its upload-session token (the canonical first step of every upload), and ingots.createDownloadLink(options) mints a fresh short-lived signed download URL validated against the allowed-host policy (the canonical first step of every download).
Use Case: Vault Management
// List, get, create, update, delete
const vaults = await client.vaults.list();
const vault = await client.vaults.get(vaultId);
const created = await client.vaults.create({ name: 'My Vault' });
await client.vaults.update(vaultId, { name: 'Renamed' });
await client.vaults.delete(vaultId);
// Unseal / seal
const session = await client.vaults.unseal(vaultId, { vmk });
await client.vaults.seal(vaultId);
// Sharing
await client.vaults.enableSharing(vaultId, vmk, { all_ingots_public: false });
await client.vaults.disableSharing(vaultId);
// Folders + ingots inside a vault
const folders = await client.folders.list(vaultId, session.vat);
const ingots = await client.ingots.list(vaultId, session.vat);
Health Checks
const status = await client.health.check();
// status.online: true when the API returns 2xx
// status.status: 'healthy' | 'unhealthy' | 'unreachable'
// status.httpStatus: HTTP status when a response was received
// status.checkedAt: Unix timestamp
// status.error: network/timeout message when unreachable
const online = await client.health.isOnline();
Errors
All SDK failures throw typed error classes, exported from the package root. Every class extends SparkVaultMobileError, which carries code, statusCode, details, and meta.
| Class | Thrown when |
|---|---|
SparkVaultMobileError | Base class for all SDK errors |
SparkVaultValidationError | Invalid input or config (400) |
SparkVaultAuthenticationError | Authentication required (401) |
SparkVaultAuthorizationError | Forbidden (403) |
PlanRequiredError | 402 PLAN_REQUIRED billing gate: the account has no active subscription; route the user into the in-app subscribe flow |
QuotaExceededError | 402 QUOTA_EXCEEDED billing gate: a pooled-capacity or seat limit was reached; resource is 'storage' | 'bandwidth' | 'seats' | 'identity' |
SparkVaultNetworkError | Network request failed |
SparkVaultTimeoutError | Request timed out (408) |
TusUploadError | Resumable upload failure: carries filename, fileSize, and phase ('create' | 'upload' | 'network' | 'timeout' | 'cancelled' | 'stalled' | 'unknown') |
IngotNotReadyError | 400 INGOT_NOT_READY: a download was attempted while the ingot is still uploading/encrypting; carries ingotStatus. Back off and retry once the ingot becomes active |
The helper gateErrorFromBody(statusCode, errorBody) is also exported: it converts a 402 response body into a typed PlanRequiredError/QuotaExceededError (or null for unrecognized gates), so subscribe/add-on UX can key off one set of error types across the JSON and tus paths.
For download-retry logic, branch on the exported type guards isIngotNotReadyError(error) and isRetryableDownloadError(error) rather than inspecting code directly.
Resources
- Source:
packages/sdk-mobile/ - npm: @sparkvault/sdk-mobile
- See also: JavaScript SDK for browser / Node.