https://github.com/TheNaubit/expo-stable-id
Persistent cross-device user identifier for Expo
https://github.com/TheNaubit/expo-stable-id
cross-device expo expo-stable-id expostableid icloud keychain persistent-id react-native stable-id stableid user-identifier
Last synced: about 1 month ago
JSON representation
Persistent cross-device user identifier for Expo
- Host: GitHub
- URL: https://github.com/TheNaubit/expo-stable-id
- Owner: TheNaubit
- License: mit
- Created: 2026-02-06T16:48:28.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-02-07T13:27:42.000Z (4 months ago)
- Last Synced: 2026-03-03T20:57:11.906Z (3 months ago)
- Topics: cross-device, expo, expo-stable-id, expostableid, icloud, keychain, persistent-id, react-native, stable-id, stableid, user-identifier
- Language: TypeScript
- Homepage:
- Size: 929 KB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
- awesome-react-native - expo-stable-id - Persistent cross-device user identifier using iCloud sync and secure local storage. (Authentication & Security / Graphics & Drawing)
README
# @nauverse/expo-stable-id
[](https://www.npmjs.com/package/@nauverse/expo-stable-id)
[](https://github.com/TheNaubit/expo-stable-id/actions/workflows/ci.yml)
[](https://opensource.org/licenses/MIT)
Persistent, cross-device user identifier for React Native/Expo. Port of [StableID](https://github.com/codykerns/StableID) (Swift) to the Expo ecosystem, big thanks to him for that awesome lib!
## How it works
`expo-stable-id` provides a persistent user identifier using **dual storage**:
- **Cloud**: [`@nauverse/expo-cloud-settings`](https://github.com/TheNaubit/expo-cloud-settings) (iCloud KVS on iOS, future Android support) - syncs across devices
- **Local**: [`expo-secure-store`](https://docs.expo.dev/versions/latest/sdk/securestore/) (Keychain on iOS, Android Keystore) - persists across app reinstalls
The ID is generated once and persisted to both storages. On subsequent launches, the stored ID is read back. When iCloud syncs a new ID from another device, the local copy is updated.
## Installation
```bash
npx expo install @nauverse/expo-stable-id @nauverse/expo-cloud-settings expo-secure-store expo-crypto
```
### Config Plugin
Add to your `app.config.ts` / `app.json`:
```ts
export default {
plugins: ['@nauverse/expo-stable-id'],
};
```
This adds the iCloud KVS entitlement required for cloud sync. Optionally pass a custom container:
```ts
export default {
plugins: [
['@nauverse/expo-stable-id', { containerIdentifier: 'com.example.shared' }],
],
};
```
## Usage
### React Hooks (Recommended)
> **Note:** `StableIdProvider` does **not** require `CloudSettingsProvider` from `@nauverse/expo-cloud-settings` as an ancestor. Internally it uses the functional API (`getString`, `setString`, `addChangeListener`) from `@nauverse/expo-cloud-settings` directly. If your app also uses `CloudSettingsProvider` for its own React hooks (`useCloudSetting*`), both providers are independent and can be placed in any order.
```tsx
import { StableIdProvider, useStableId } from '@nauverse/expo-stable-id';
function App() {
return (
);
}
function MyComponent() {
const [id, { identify, generateNewId }] = useStableId();
return (
Stable ID: {id ?? 'Loading...'}
generateNewId()} />
identify('user-123')} />
);
}
```
#### Provider Config
```tsx
import { StandardGenerator, ShortIDGenerator } from '@nauverse/expo-stable-id';
// Use short 8-char IDs instead of UUIDs
// Provide a known ID, force it even if one exists
// Provide a fallback ID, prefer any stored value
```
### Functional API
```ts
import {
configure,
getId,
identify,
generateNewId,
isConfigured,
hasStoredId,
addChangeListener,
setWillChangeHandler,
} from '@nauverse/expo-stable-id';
// Initialize (call once at app startup)
const id = await configure();
// Or with options
const id = await configure({
id: 'fallback-id',
policy: 'preferStored',
generator: new ShortIDGenerator(),
});
// Read current ID (sync, from cache)
const currentId = getId();
// Change ID
identify('new-user-id');
// Generate a new random ID
const newId = generateNewId();
// Check state
isConfigured(); // boolean
await hasStoredId(); // boolean
// Listen for changes
const subscription = addChangeListener((event) => {
console.log(`ID changed: ${event.previousId} -> ${event.newId} (${event.source})`);
});
subscription.remove();
// Intercept changes before they apply (identify, generateNewId, cloud sync)
setWillChangeHandler((currentId, candidateId) => {
// Return modified ID, or null to accept candidate as-is
return candidateId;
});
```
### Real-World Example: Shared ID for PostHog + RevenueCat
Use the same stable ID across your analytics and payment provider so user events are always linked:
```tsx
import { StableIdProvider, useStableId } from '@nauverse/expo-stable-id';
import PostHog from 'posthog-react-native';
import Purchases from 'react-native-purchases';
import { useEffect } from 'react';
function App() {
return (
{/* rest of your app */}
);
}
function IdentifyProviders() {
const [id] = useStableId();
useEffect(() => {
if (!id) return;
// Same ID in both services
PostHog.identify(id);
Purchases.logIn(id);
}, [id]);
return null;
}
```
## ID Generators
| Generator | Output | Example |
|-----------|--------|---------|
| `StandardGenerator` (default) | UUID v4 | `a1b2c3d4-e5f6-4789-abcd-ef0123456789` |
| `ShortIDGenerator` | 8-char alphanumeric | `xK9mP2nQ` |
Custom generators implement the `IDGenerator` interface:
```ts
import type { IDGenerator } from '@nauverse/expo-stable-id';
const myGenerator: IDGenerator = {
generate: () => `prefix-${Date.now()}`,
};
```
## Policies
| Policy | Behavior |
|--------|----------|
| `'forceUpdate'` (default) | Always use the provided `id` (if given) |
| `'preferStored'` | Use stored ID if available, fall back to provided `id` |
## Platform Support
| Feature | iOS | Android |
|---------|-----|---------|
| Local storage (Keychain/Keystore) | Yes | Yes |
| Cloud sync (iCloud KVS) | Yes | Coming soon |
| ID generation | Yes | Yes |
## API Reference
### Functional API
| Function | Returns | Description |
|----------|---------|-------------|
| `configure(config?)` | `Promise` | Initialize and get/create stable ID |
| `getId()` | `string \| null` | Current cached ID (sync) |
| `identify(id)` | `void` | Set a specific ID |
| `generateNewId()` | `string` | Generate and persist a new ID |
| `isConfigured()` | `boolean` | Whether `configure()` has been called |
| `hasStoredId()` | `Promise` | Whether an ID exists in storage |
| `addChangeListener(cb)` | `{ remove: () => void }` | Subscribe to ID changes |
| `setWillChangeHandler(fn)` | `void` | Intercept all ID changes before they apply |
### React API
| Export | Description |
|--------|-------------|
| `StableIdProvider` | Context provider, call `configure()` internally |
| `useStableId()` | `[id, { identify, generateNewId }]` |
### Types
```ts
type IDPolicy = 'preferStored' | 'forceUpdate';
type ChangeSource = 'cloud' | 'manual';
interface IDGenerator {
generate(): string;
}
interface StableIdConfig {
readonly id?: string;
readonly generator?: IDGenerator;
readonly policy?: IDPolicy;
}
interface StableIdChangeEvent {
readonly previousId: string | null;
readonly newId: string;
readonly source: ChangeSource;
}
```
## Feature Mapping from StableID (Swift)
| StableID (Swift) | expo-stable-id | Notes |
|------------------|----------------|-------|
| `StableID.configure(id?, generator, policy)` | `configure(config?)` | Same semantics |
| `StableID.id` | `getId()` / `useStableId()[0]` | Sync cached read |
| `StableID.identify(id:)` | `identify(id)` | Writes both storages |
| `StableID.generateNewID()` | `generateNewId()` | Uses configured generator |
| `StableID.isConfigured` | `isConfigured()` | Static check |
| `StableID.hasStoredID` | `hasStoredId()` | Checks both storages |
| `StableID.set(delegate:)` | `addChangeListener()` + `setWillChangeHandler()` | JS-idiomatic |
| `StandardGenerator` | `StandardGenerator` | UUID v4 |
| `ShortIDGenerator` | `ShortIDGenerator` | 8-char alphanumeric |
## License
MIT