Check access
Gate features on the current person's perks, online and offline.
Gate features on perks: a person has access when they hold an active entitlement for the perk's slug. Perks come from the products the person bought, so one check covers subscriptions and one-time purchases. See Products and perks.
The SDK decides what the app shows. Protect anything reachable through your own API on your server, with the server library for Node.js, PHP, Go or Rust, or over HTTP.
Gate a feature
import { voidhash } from "../src/lib/voidhash";
export function PremiumGate() {
const { hasAccess, isLoading } = voidhash.useHasPerk("premium");
if (isLoading) return null;
return hasAccess ? <PremiumContent /> : <UpgradePrompt />;
}| Field | Meaning |
|---|---|
hasAccess | true when the person holds an active entitlement for the perk. |
grant | The entitlement behind hasAccess, or null. |
isLoading | true while the SDK initializes and until the first check answers. |
isStale | true while the answer hasn't been confirmed by the server recently, or nothing is cached. |
error | Set only when there was nothing to answer from, such as AUTHENTICATION_FAILED. |
refetch | Checks again. |
The hook re-renders when a refresh lands, including after a purchase or restore, and checks again
for the new person after identify() or reset().
Outside React, client.hasPerk() resolves to a Result:
const result = await voidhash.client.hasPerk("premium");
if (result.isOk() && result.value.hasAccess) unlock();Offline and stale answers
A failed refresh isn't proof that the person has no access, so neither check fails because of the network:
- Cached access stays
truewhile the device is offline, withisStaleset. - With nothing cached,
hasAccessisfalseandisStaleistrue: there is no evidence either way. - If the publishable key was rejected and nothing is cached, the check fails once with
AUTHENTICATION_FAILED.
Treat an unconfirmed "no" differently from a denial:
const { hasAccess, isLoading, isStale, refetch } = voidhash.useHasPerk("premium");
if (isLoading) return null;
if (hasAccess) return <PremiumContent />;
if (isStale) return <AccessUnconfirmed onRetry={refetch} />;
return <UpgradePrompt />;client.hasPerk() also says how fresh its answer is:
| Field | Meaning |
|---|---|
isStale | The cached answer is older than 5 minutes. |
isExpired | The cached answer is older than 2 days. Access from it is a best guess. |
reason | fresh: confirmed by the server. refresh-in-flight: from cache while a refresh runs. refresh-failed: from cache because the refresh failed. no-cache: nothing seen yet for this person. |
Before something you can't take back, such as granting a consumable, force a server check and act only on a confirmed answer:
const result = await voidhash.client.hasPerk("premium", { forceFetch: true });
if (result.isOk() && result.value.reason === "fresh" && result.value.hasAccess) {
grantCoins();
}Read the current person
For account screens, read the whole person:
const { data: person, error, isLoading, refetch } = voidhash.useCurrentPerson();data is null until the first snapshot loads and while the client is disabled.
client.getCurrentPerson() resolves to { person, isStale, isExpired, reason }.
| Field | Contents |
|---|---|
personId, distinctId | The person's IDs. See People and identity. |
email, name | Profile fields, or null. |
entitlements.grants | Entitlements, described below. |
subscriptions.current | { productId, status, subscriptionId, expiresAt } or null. status is active, trialing or canceled; see Statuses. |
subscriptions.history | Every subscription, with startsAt, expiresAt, canceledAt, isTrial and status (active, trialing, canceled or expired). |
purchases.history | Every purchase, with productId, type (one_time or subscription) and createdAt. |
Dates are ISO 8601 strings. Use subscriptions.current for plan and renewal UI, and perks for
access. Subscription states are explained in Subscription lifecycle.
Each entry in entitlements.grants has:
| Field | Meaning |
|---|---|
perkSlug | The perk's slug. null when the perk was deleted. |
perkId | The perk's internal ID. |
status | active or expired. |
source | subscription or purchase. |
sourceId | The subscription or purchase that created it. |
expiresAt | When access ends, or null for access that doesn't expire. |
Refresh behavior
The SDK refreshes the person at launch, on foreground, after purchases and restores, and when the
identity changes. Between refreshes, a snapshot younger than 5 minutes answers without a request; an
older one answers from cache if a refresh takes longer than 500 ms. To refresh now, call
refetch() or getCurrentPerson({ forceFetch: true }). resetCache() clears the cached person.
Environments
The publishable key the client uses decides which purchases unlock perks. With the production key
(vh_pk_…), production purchases count, plus store sandbox purchases such as TestFlight and Google
Play test purchases while they
unlock production apps.
With the test key (vh_pk_test_…), store sandbox, simulated and test mode purchases count, never
production ones. Use the test key in development builds and the production key in release builds.
Next steps
- Check access from your backend with Node.js, PHP, Go, Rust or over HTTP
- Identify signed-in users
- Troubleshoot a purchase that doesn't unlock
Something wrong or missing on this page? Tell us by email or on Discord.