Client Configuration

Configure createVoidhashClient, understand what happens on Provider mount, and gate your UI on initialization.

The SDK is bootstrapped with a single factory call. createVoidhashClient(publishableKey, options) builds one client instance and returns a bag containing the Provider, the raw client, and a set of hooks that are all bound to that instance. Create it once in a module, export the result, and import it wherever you need it:

utils/voidhash/client.ts
import { createVoidhashClient } from "@voidhash/react-native";

export const voidhash = createVoidhashClient("vh_pk_your_publishable_key", {
  debug: __DEV__,
});
app/_layout.tsx
import { voidhash } from "utils/voidhash/client";

export default function RootLayout() {
  return <voidhash.Provider>{/* Your app content */}</voidhash.Provider>;
}

Because the hooks are created by the factory, they are always wired to the right client — there is no global singleton to configure and no client prop to thread through your tree. The returned object contains:

  • Provider — React provider that initializes the client on mount
  • client — the underlying VoidhashClient instance for imperative calls (identify, capture, purchase, ...)
  • useVoidhash — access to { client, isInitialized } inside the tree
  • useCurrentPerson, useFeatureFlags, usePaywallByLocation, useProducts, usePurchase — data hooks bound to this client

There is no schema argument

Older versions of the SDK accepted a schema object as the second argument. That is gone: your schema (products, locations, perks) lives on the server and is fetched automatically when the Provider mounts. Type-safe slugs come from a generated voidhash.gen.d.ts instead — see Schema & Generated Types.

Options

All options are optional. Defaults are applied inside createVoidhashClient.

OptionTypeDefault
baseUrlstring"https://api.voidhash.com"
debugbooleanfalse
distinctIdstringundefined (anonymous id)
ingestUrlstringderived from baseUrl
readOnlybooleanfalse
schemestringExpo config scheme
unstable_swallowErrorsbooleanfalse

baseUrl

The Voidhash API origin. Only override this when pointing the SDK at a self-hosted or local instance.

debug

Enables verbose HTTP request/response logging (sensitive headers are redacted). Useful during integration; keep it off in production builds — passing __DEV__ is a good default.

distinctId

Seeds the identity the client initializes with. When omitted, the SDK reuses the persisted distinct id from a previous session, or generates and persists a new anonymous id (prefixed vh:anon:). You normally don't set this — call client.identify() after your own sign-in instead (see Identifying Users).

ingestUrl

Overrides the base origin for analytics event delivery. By default events are sent to the same host as baseUrl (the ingest endpoint lives under the /i/v1/... path prefix there), so you only need this when running a separate local or test ingest server. See Analytics.

readOnly

Puts the client in observer mode: purchase initiation is blocked (purchase and setPersonAttributesSync throw), and the SDK observes and syncs store transactions without ever acknowledging or finishing them. Use this to run Voidhash alongside another billing SDK — see Observer Mode.

scheme

The deep-link scheme used for paywall callback URLs (yourscheme://voidhash/callback/success and .../error). In an Expo app this defaults to the scheme from your app config, so you rarely set it explicitly. If no scheme can be resolved from either source, createVoidhashClient throws a SchemeNotSetError immediately.

unstable_swallowErrors

When enabled, failures in side-effect methods (init, end, identify, reset, signOut, setPersonAttributes, restorePurchases, flush, and the iOS sheet helpers) log a [voidhash] console warning instead of rejecting. Methods that return data you render (getCurrentPerson, getProducts, getFeatureFlags, getPaywallForLocation, purchase, setPersonAttributesSync) still reject so you never silently show wrong state. This is an alpha-era convenience for keeping an app alive while integrating; prefer real error handling — see Error Handling.

What happens on Provider mount

The Provider calls client.init() in an effect on mount and flips isInitialized to true once it resolves. init() performs, in order (network calls run concurrently where possible):

  1. Identity — resolves the distinct id: the distinctId option if provided, otherwise the persisted id from a previous session, otherwise a freshly generated anonymous id.
  2. Schema fetch — fetches the runtime schema from the server through a stale-while-revalidate cache. A cached schema keeps the app working offline; a cold cache combined with an unreachable server is fatal and rejects init() with FailedToFetchSchemaError.
  3. Person prefetch — fetches the current person snapshot and publishes it, so useCurrentPerson has data without a hook-driven refetch.
  4. Transaction observer — attaches the native store transaction observer (StoreKit 2 / Play Billing) and kicks off a background reconciliation of previously observed transactions.
  5. Analytics — transfers any events you captured before initialization from the pre-init buffer into the analytics queue, captures automatic startup events ($app_installed / $app_updated / $app_opened), wires app lifecycle events, and triggers a background flush.

client.capture() is safe to call at any time — pre-init events are buffered and delivered after step 5. Every other client method throws a VOIDHASH_CLIENT_NOT_INITIALIZED error when called before init() completes.

Gating on initialization

The bound hooks handle the gate for you: they wait for isInitialized internally. For imperative client.* calls from your own components, check the gate first with useVoidhash():

import { voidhash } from "utils/voidhash/client";

function AccountActions() {
  const { client, isInitialized } = voidhash.useVoidhash();

  if (!isInitialized) {
    return null;
  }

  return <Button title="Restore purchases" onPress={() => client.restorePurchases()} />;
}

useVoidhash() throws if called outside the Provider, so a missing wrapper fails loudly rather than returning stale state.

Shutting down

client.end() flushes queued analytics, closes the native store connection, and removes the lifecycle subscription. After it resolves, isInitialized is false again. You rarely need this in an app — the client is designed to live for the process lifetime — but it is useful in tests or when tearing a client down deliberately:

await voidhash.client.end();