Mobile SDK v1

What are you trying to do?

Installation

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

Native Identity dialog ships as a subpath

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.

ModulePurpose
SparkVaultIdentityDialogNative Identity popup driven by Identity Product config, branding, and enabled methods
client.products.identityIdentity Product config, OTP/passkey login, JWKS/JWT verification, and second-factor submission
client.accountFirst-party Core session: Identity-token exchange, signup completion, and profile/account
client.vaultsVault CRUD, unseal/seal, sharing, access-log retention, and upload portal/widget settings
client.foldersFolder CRUD, breadcrumbs, folder moves, and ingot moves
client.ingotsList/search/get, upload/replace/rename/delete, sharing, access logs, downloads
client.healthRoot API availability checks
client.sparksSparkSync ephemeral secrets: list/get/create/delete and SparkLink sharing (share/getShare/unshare)
client.entropyHSM-backed entropy generation
client.notifyMetadata-only notifications inbox (getInbox, getUnreadCount, markState) plus Expo device push registration (registerDevice, unregisterDevice)
client.billingSubscription and usage, Stripe customer portal, and native in-app purchase validation
client.usersAccount 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.

typescript
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 }),
  },
});
Adapter contract
  • tokenStorage and identityAccountId are required: construction throws if missing (identityAccountId must start with acc_).
  • fetch falls back to globalThis.fetch.
  • fileReader is required for ingots.uploadFromUri.
  • fileDownloader is required for ingots.downloadToFile.
  • logger is optional: defaults to a no-op logger.
  • passkeyProvider is 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:

FieldDefault
apiBaseUrlhttps://api.sparkvault.com/v1
identityBaseUrlhttps://auth.sparkvault.com
preloadIdentityConfigtrue: prefetches the Identity Product config at construction
timeoutMs30000: general request timeout
fileTransferTimeoutMs300000: file download timeout
tusPostTimeoutMs30000: tus upload-creation timeout
tusChunkTimeoutMs120000: per-chunk upload timeout
tusChunkSizeBytes5242880 (5 MiB)
allowedDownloadHostPatternsSparkVault 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.

Identity Product Endpoint

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

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

Security Critical

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.

Third-Party App Sessions

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.

typescript
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

typescript
// 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

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

ClassThrown when
SparkVaultMobileErrorBase class for all SDK errors
SparkVaultValidationErrorInvalid input or config (400)
SparkVaultAuthenticationErrorAuthentication required (401)
SparkVaultAuthorizationErrorForbidden (403)
PlanRequiredError402 PLAN_REQUIRED billing gate: the account has no active subscription; route the user into the in-app subscribe flow
QuotaExceededError402 QUOTA_EXCEEDED billing gate: a pooled-capacity or seat limit was reached; resource is 'storage' | 'bandwidth' | 'seats' | 'identity'
SparkVaultNetworkErrorNetwork request failed
SparkVaultTimeoutErrorRequest timed out (408)
TusUploadErrorResumable upload failure: carries filename, fileSize, and phase ('create' | 'upload' | 'network' | 'timeout' | 'cancelled' | 'stalled' | 'unknown')
IngotNotReadyError400 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