Production-grade cloud backup SDK for Expo / React Native wallet apps.
Stores an encrypted master key in Google Drive (appDataFolder) or iCloud via a clean provider abstraction.
npm install @tetherto/wdk-backup-cloud-react-native react-native-cloud-storageExpo note: the bundled Google Drive and iCloud providers depend on
react-native-cloud-storage, which requires native modules. Use a custom dev build — it does NOT work with Expo Go.
| Platform | Cloud Target | Requirement |
|---|---|---|
| iOS / macOS | iCloud | iCloud Drive enabled in Settings; user signed in |
| Android / iOS | Google Drive | OAuth2 access token with drive.appdata scope |
This SDK performs NO OAuth flows. You must supply a valid token.
import {
CloudBackup,
GoogleDriveProvider,
CloudAuthError,
CloudUnavailableError,
CloudStorageError,
CloudValidationError,
} from "@tetherto/wdk-backup-cloud-react-native";
// Token must be obtained by the caller (e.g., via expo-auth-session)
const provider = new GoogleDriveProvider({ accessToken: "<your_token>" });
const cloud = new CloudBackup(provider);
// Upload (metadata must include version; savedAt is auto-filled if omitted)
const result = await cloud.uploadEncryptedKey(encryptedKey, { version: 1 });
// Download — returns the full CloudEncryptionKeyFile or null
const backup = await cloud.downloadEncryptedKey(); // CloudEncryptionKeyFile | null
if (backup) {
console.log(backup.encryptionKey, backup.version);
}
// Delete
await cloud.deleteBackup();
// Availability check
const available = await cloud.isAvailable(); // boolean
// Existence check (without downloading)
const hasBackup = await cloud.exists(); // booleanimport {
CloudBackup,
ICloudProvider,
} from "@tetherto/wdk-backup-cloud-react-native";
const provider = new ICloudProvider(); // default path: wallet_backup_key.json
const cloud = new CloudBackup(provider);
await cloud.uploadEncryptedKey(encryptedKey, { version: 1 });import {
CloudValidationError,
CloudAuthError,
CloudUnavailableError,
CloudStorageError,
} from "@tetherto/wdk-backup-cloud-react-native";
try {
await cloud.uploadEncryptedKey(encryptedKey, { version: 1 });
} catch (err) {
if (err instanceof CloudValidationError) {
// Empty or invalid key passed by caller
} else if (err instanceof CloudAuthError) {
// Re-authenticate (refresh token or prompt sign-in) and retry
} else if (err instanceof CloudUnavailableError) {
// No network, iCloud disabled, Drive service down
} else if (err instanceof CloudStorageError) {
// Quota exceeded, I/O error, malformed response
}
}Each error carries a machine-readable code discriminant:
| Class | code |
|---|---|
CloudValidationError |
CLOUD_VALIDATION_ERROR |
CloudAuthError |
CLOUD_AUTH_ERROR |
CloudUnavailableError |
CLOUD_UNAVAILABLE |
CloudStorageError |
CLOUD_STORAGE_ERROR |
new CloudBackup(provider: CloudProvider)| Method | Signature | Description |
|---|---|---|
uploadEncryptedKey |
(key, metadata) => Promise<CloudEncryptionKeyFile | null> |
Validate + upload. Returns the written payload. Throws CloudValidationError on empty key. |
downloadEncryptedKey |
() => Promise<CloudEncryptionKeyFile | null> |
Download full backup payload or null if no backup exists. |
deleteBackup |
() => Promise<void> |
Delete backup (idempotent). |
isAvailable |
() => Promise<boolean> |
Lightweight probe — never throws. |
exists |
() => Promise<boolean> |
Check if backup file exists without downloading — never throws. |
new GoogleDriveProvider(config: GoogleDriveConfig)
interface GoogleDriveConfig {
accessToken: string; // OAuth2 token — drive.appdata scope required
filePath?: string; // default: "wallet_backup_key.json"
cloudEmail?: string; // stored inside the backup file for traceability
}- File stored in
appDataFolderaswallet_backup_key.json(configurable viafilePath) - Uses
react-native-cloud-storagewithCloudStorageScope.AppData - Verifies file existence after every write
new ICloudProvider(config?: ICloudConfig)
interface ICloudConfig {
filePath?: string; // default: "wallet_backup_key.json"
cloudEmail?: string; // stored inside the backup file for traceability
}- Uses
react-native-cloud-storage(install it alongside this package) - Uses
CloudStorageScope.AppData(app-specific hidden folder) - Checks iCloud availability before every operation
- Verifies file existence after every write
- Handles: iCloud not available, user not signed in, quota errors
import type {
CloudProvider,
CloudEncryptionKeyFile,
} from "@tetherto/wdk-backup-cloud-react-native";
class MyCustomProvider implements CloudProvider {
async upload(
key: string,
metadata: Record<string, unknown>,
): Promise<CloudEncryptionKeyFile | null> {
/* ... */
}
async download(): Promise<CloudEncryptionKeyFile | null> {
/* ... */
}
async delete(): Promise<void> {
/* ... */
}
async isAvailable(): Promise<boolean> {
/* ... */
}
async exists(): Promise<boolean> {
/* ... */
}
}
const cloud = new CloudBackup(new MyCustomProvider());Both providers write the same JSON payload (CloudEncryptionKeyFile):
{
"encryptionKey": "<encrypted_wallet_master_key>",
"savedAt": "2026-02-25T00:00:00.000Z",
"platform": "ios",
"version": 1,
"cloudEmail": "user@example.com"
}| Field | Type | Description |
|---|---|---|
encryptionKey |
string |
The encrypted wallet master key |
savedAt |
string |
ISO-8601 UTC timestamp when the backup was saved |
platform |
"ios" | "android" |
Platform that created this backup |
version |
number |
Backup version number |
cloudEmail |
string |
Cloud user email that owns this backup |
- Never logs the encrypted key or access token
- No AsyncStorage — entirely in-request-lifecycle
- No local persistence — this SDK does not store backups outside provider calls
- No OAuth flows implemented — the caller owns credential management
- Error messages strip sensitive values
npm run build # Outputs dist/ (CJS + ESM + .d.ts)
npm test # Jest test suite
npm run test:coverage # Coverage report with thresholds
npm run typecheck # tsc --noEmitFirst publish:
npm login
npm run build
npm publish --access publicPublishing updates:
# 1. Make your code changes
# 2. Bump version (choose one based on semver):
npm version patch # 1.0.0 → 1.0.1 (bug fixes)
npm version minor # 1.0.0 → 1.1.0 (new features, backward compatible)
npm version major # 1.0.0 → 2.0.0 (breaking changes)
# 3. Build and publish
npm run build
npm publish --access publicOnce the package reaches 1.0.0, consumers using ^1.0.0 will receive patch/minor updates automatically on npm install.
src/
types.ts # CloudProvider interface + config types
errors.ts # CloudUnavailableError, CloudAuthError,
│ # CloudStorageError, CloudValidationError
cloudBackup.ts # Public CloudBackup wrapper
providers/
googleDriveProvider.ts # react-native-cloud-storage (AppData scope)
iCloudProvider.ts # react-native-cloud-storage
index.ts # Public barrel (named exports only)
__tests__/
errors.test.ts
cloudBackup.test.ts
googleDriveProvider.test.ts
iCloudProvider.test.ts
__mocks__/
react-native-cloud-storage.ts
examples/
usage.ts
import { BackendBackupClient } from "@tetherto/wdk-backup-remote";
import type { SeedItem, EntropyItem } from "@tetherto/wdk-backup-remote";
import {
CloudBackup,
GoogleDriveProvider,
} from "@tetherto/wdk-backup-cloud-react-native";
const backend = new BackendBackupClient({
baseUrl: "https://api.mywallet.com",
});
const cloud = new CloudBackup(new GoogleDriveProvider({ accessToken }));
// Upload (seed, entropy, and cloud key)
await backend.uploadSeed({
seed: encryptedSeed,
authToken,
metadata: { device: "ios" },
});
await backend.uploadEntropy({ entropy: encryptedEntropy, authToken });
await cloud.uploadEncryptedKey(encryptedMasterKey, { version: 1 });
// Retrieve — backend returns full arrays of SeedItem[] / EntropyItem[]
const seeds: SeedItem[] = await backend.getSeed(authToken); // [] if no backup
const entropies: EntropyItem[] = await backend.getEntropy(authToken); // [] if no backup
const backup = await cloud.downloadEncryptedKey(); // CloudEncryptionKeyFile | nullApache-2.0