{"id":48362098,"url":"https://github.com/TheNaubit/expo-stable-id","last_synced_at":"2026-04-21T03:01:27.702Z","repository":{"id":336954927,"uuid":"1151588085","full_name":"TheNaubit/expo-stable-id","owner":"TheNaubit","description":"Persistent cross-device user identifier for Expo","archived":false,"fork":false,"pushed_at":"2026-02-07T13:27:42.000Z","size":951,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-03T20:57:11.906Z","etag":null,"topics":["cross-device","expo","expo-stable-id","expostableid","icloud","keychain","persistent-id","react-native","stable-id","stableid","user-identifier"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TheNaubit.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-06T16:48:28.000Z","updated_at":"2026-02-27T13:07:16.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/TheNaubit/expo-stable-id","commit_stats":null,"previous_names":["thenaubit/expo-stable-id"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/TheNaubit/expo-stable-id","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheNaubit%2Fexpo-stable-id","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheNaubit%2Fexpo-stable-id/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheNaubit%2Fexpo-stable-id/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheNaubit%2Fexpo-stable-id/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TheNaubit","download_url":"https://codeload.github.com/TheNaubit/expo-stable-id/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheNaubit%2Fexpo-stable-id/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32074812,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-21T02:38:07.213Z","status":"ssl_error","status_checked_at":"2026-04-21T02:38:06.559Z","response_time":128,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cross-device","expo","expo-stable-id","expostableid","icloud","keychain","persistent-id","react-native","stable-id","stableid","user-identifier"],"created_at":"2026-04-05T13:00:29.645Z","updated_at":"2026-04-21T03:01:27.690Z","avatar_url":"https://github.com/TheNaubit.png","language":"TypeScript","funding_links":[],"categories":["Authentication \u0026 Security"],"sub_categories":["Graphics \u0026 Drawing"],"readme":"# @nauverse/expo-stable-id\n\n[![npm version](https://img.shields.io/npm/v/@nauverse/expo-stable-id.svg)](https://www.npmjs.com/package/@nauverse/expo-stable-id)\n[![CI](https://github.com/TheNaubit/expo-stable-id/actions/workflows/ci.yml/badge.svg)](https://github.com/TheNaubit/expo-stable-id/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nPersistent, 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!\n\n## How it works\n\n`expo-stable-id` provides a persistent user identifier using **dual storage**:\n\n- **Cloud**: [`@nauverse/expo-cloud-settings`](https://github.com/TheNaubit/expo-cloud-settings) (iCloud KVS on iOS, future Android support) - syncs across devices\n- **Local**: [`expo-secure-store`](https://docs.expo.dev/versions/latest/sdk/securestore/) (Keychain on iOS, Android Keystore) - persists across app reinstalls\n\nThe 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.\n\n## Installation\n\n```bash\nnpx expo install @nauverse/expo-stable-id @nauverse/expo-cloud-settings expo-secure-store expo-crypto\n```\n\n### Config Plugin\n\nAdd to your `app.config.ts` / `app.json`:\n\n```ts\nexport default {\n  plugins: ['@nauverse/expo-stable-id'],\n};\n```\n\nThis adds the iCloud KVS entitlement required for cloud sync. Optionally pass a custom container:\n\n```ts\nexport default {\n  plugins: [\n    ['@nauverse/expo-stable-id', { containerIdentifier: 'com.example.shared' }],\n  ],\n};\n```\n\n## Usage\n\n### React Hooks (Recommended)\n\n\u003e **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.\n\n```tsx\nimport { StableIdProvider, useStableId } from '@nauverse/expo-stable-id';\n\nfunction App() {\n  return (\n    \u003cStableIdProvider\u003e\n      \u003cMyComponent /\u003e\n    \u003c/StableIdProvider\u003e\n  );\n}\n\nfunction MyComponent() {\n  const [id, { identify, generateNewId }] = useStableId();\n\n  return (\n    \u003cView\u003e\n      \u003cText\u003eStable ID: {id ?? 'Loading...'}\u003c/Text\u003e\n      \u003cButton title=\"New ID\" onPress={() =\u003e generateNewId()} /\u003e\n      \u003cButton title=\"Set Custom\" onPress={() =\u003e identify('user-123')} /\u003e\n    \u003c/View\u003e\n  );\n}\n```\n\n#### Provider Config\n\n```tsx\nimport { StandardGenerator, ShortIDGenerator } from '@nauverse/expo-stable-id';\n\n// Use short 8-char IDs instead of UUIDs\n\u003cStableIdProvider config={{ generator: new ShortIDGenerator() }}\u003e\n\n// Provide a known ID, force it even if one exists\n\u003cStableIdProvider config={{ id: 'known-user-id', policy: 'forceUpdate' }}\u003e\n\n// Provide a fallback ID, prefer any stored value\n\u003cStableIdProvider config={{ id: 'fallback-id', policy: 'preferStored' }}\u003e\n```\n\n### Functional API\n\n```ts\nimport {\n  configure,\n  getId,\n  identify,\n  generateNewId,\n  isConfigured,\n  hasStoredId,\n  addChangeListener,\n  setWillChangeHandler,\n} from '@nauverse/expo-stable-id';\n\n// Initialize (call once at app startup)\nconst id = await configure();\n\n// Or with options\nconst id = await configure({\n  id: 'fallback-id',\n  policy: 'preferStored',\n  generator: new ShortIDGenerator(),\n});\n\n// Read current ID (sync, from cache)\nconst currentId = getId();\n\n// Change ID\nidentify('new-user-id');\n\n// Generate a new random ID\nconst newId = generateNewId();\n\n// Check state\nisConfigured(); // boolean\nawait hasStoredId(); // boolean\n\n// Listen for changes\nconst subscription = addChangeListener((event) =\u003e {\n  console.log(`ID changed: ${event.previousId} -\u003e ${event.newId} (${event.source})`);\n});\nsubscription.remove();\n\n// Intercept changes before they apply (identify, generateNewId, cloud sync)\nsetWillChangeHandler((currentId, candidateId) =\u003e {\n  // Return modified ID, or null to accept candidate as-is\n  return candidateId;\n});\n```\n\n### Real-World Example: Shared ID for PostHog + RevenueCat\n\nUse the same stable ID across your analytics and payment provider so user events are always linked:\n\n```tsx\nimport { StableIdProvider, useStableId } from '@nauverse/expo-stable-id';\nimport PostHog from 'posthog-react-native';\nimport Purchases from 'react-native-purchases';\nimport { useEffect } from 'react';\n\nfunction App() {\n  return (\n    \u003cStableIdProvider\u003e\n      \u003cIdentifyProviders /\u003e\n      {/* rest of your app */}\n    \u003c/StableIdProvider\u003e\n  );\n}\n\nfunction IdentifyProviders() {\n  const [id] = useStableId();\n\n  useEffect(() =\u003e {\n    if (!id) return;\n\n    // Same ID in both services\n    PostHog.identify(id);\n    Purchases.logIn(id);\n  }, [id]);\n\n  return null;\n}\n```\n\n## ID Generators\n\n| Generator | Output | Example |\n|-----------|--------|---------|\n| `StandardGenerator` (default) | UUID v4 | `a1b2c3d4-e5f6-4789-abcd-ef0123456789` |\n| `ShortIDGenerator` | 8-char alphanumeric | `xK9mP2nQ` |\n\nCustom generators implement the `IDGenerator` interface:\n\n```ts\nimport type { IDGenerator } from '@nauverse/expo-stable-id';\n\nconst myGenerator: IDGenerator = {\n  generate: () =\u003e `prefix-${Date.now()}`,\n};\n```\n\n## Policies\n\n| Policy | Behavior |\n|--------|----------|\n| `'forceUpdate'` (default) | Always use the provided `id` (if given) |\n| `'preferStored'` | Use stored ID if available, fall back to provided `id` |\n\n## Platform Support\n\n| Feature | iOS | Android |\n|---------|-----|---------|\n| Local storage (Keychain/Keystore) | Yes | Yes |\n| Cloud sync (iCloud KVS) | Yes | Coming soon |\n| ID generation | Yes | Yes |\n\n## API Reference\n\n### Functional API\n\n| Function | Returns | Description |\n|----------|---------|-------------|\n| `configure(config?)` | `Promise\u003cstring\u003e` | Initialize and get/create stable ID |\n| `getId()` | `string \\| null` | Current cached ID (sync) |\n| `identify(id)` | `void` | Set a specific ID |\n| `generateNewId()` | `string` | Generate and persist a new ID |\n| `isConfigured()` | `boolean` | Whether `configure()` has been called |\n| `hasStoredId()` | `Promise\u003cboolean\u003e` | Whether an ID exists in storage |\n| `addChangeListener(cb)` | `{ remove: () =\u003e void }` | Subscribe to ID changes |\n| `setWillChangeHandler(fn)` | `void` | Intercept all ID changes before they apply |\n\n### React API\n\n| Export | Description |\n|--------|-------------|\n| `StableIdProvider` | Context provider, call `configure()` internally |\n| `useStableId()` | `[id, { identify, generateNewId }]` |\n\n### Types\n\n```ts\ntype IDPolicy = 'preferStored' | 'forceUpdate';\ntype ChangeSource = 'cloud' | 'manual';\n\ninterface IDGenerator {\n  generate(): string;\n}\n\ninterface StableIdConfig {\n  readonly id?: string;\n  readonly generator?: IDGenerator;\n  readonly policy?: IDPolicy;\n}\n\ninterface StableIdChangeEvent {\n  readonly previousId: string | null;\n  readonly newId: string;\n  readonly source: ChangeSource;\n}\n```\n\n## Feature Mapping from StableID (Swift)\n\n| StableID (Swift) | expo-stable-id | Notes |\n|------------------|----------------|-------|\n| `StableID.configure(id?, generator, policy)` | `configure(config?)` | Same semantics |\n| `StableID.id` | `getId()` / `useStableId()[0]` | Sync cached read |\n| `StableID.identify(id:)` | `identify(id)` | Writes both storages |\n| `StableID.generateNewID()` | `generateNewId()` | Uses configured generator |\n| `StableID.isConfigured` | `isConfigured()` | Static check |\n| `StableID.hasStoredID` | `hasStoredId()` | Checks both storages |\n| `StableID.set(delegate:)` | `addChangeListener()` + `setWillChangeHandler()` | JS-idiomatic |\n| `StandardGenerator` | `StandardGenerator` | UUID v4 |\n| `ShortIDGenerator` | `ShortIDGenerator` | 8-char alphanumeric |\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FTheNaubit%2Fexpo-stable-id","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FTheNaubit%2Fexpo-stable-id","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FTheNaubit%2Fexpo-stable-id/lists"}