Check access
Gate features using the current customer's active perk grants.
Use the current person's active perk grants to decide what a customer can access in your app. The person snapshot also carries subscription state and purchase history, but perk grants are what you gate on.
The SDK decides what to show on the device. It cannot protect anything a customer can reach by calling your API directly. Those checks belong on your server. See Check access from your backend.
Gate a feature
A perk is available when the person holds a grant for it with status active. The built-in
useHasPerk hook checks whether the current customer holds such a grant:
const { hasAccess, isLoading } = voidhash.useHasPerk("premium");
if (isLoading) return null;
return hasAccess ? <PremiumContent /> : <UpgradePrompt />;The hook returns these fields.
| Field | Meaning |
|---|---|
hasAccess | true when the person holds an active grant for the perk. |
grant | The active grant behind hasAccess, or null. |
isLoading | true until the first snapshot has loaded. |
isStale | true when hasAccess was served from cache because a refresh failed. |
error | The refresh error, if any. |
refetch | A function that forces a refresh. |
Outside React, the client exposes the same check as an async call:
const { hasAccess, isStale } = await voidhash.client.hasPerk("premium");Gate on perks, not subscription status
Subscription status cannot tell you which features a product unlocks. It also misses access that comes from one-time purchases or manual grants. Active perk grants are the source of truth for access control.
Offline and failure behavior
A failed refresh, whether the device is offline or the server returned an error, is not proof that the customer has no access.
Both check APIs fail open with known-good data. That means they keep answering from evidence they already have rather than answering a confident "no":
- If a cached snapshot shows an active grant,
hasAccessstaystrueandisStaleis set. - If there is no cached evidence of access,
client.hasPerkfails with the refresh error. The hook reportshasAccess: falsewitherrorset.
Choose a fallback that fits each feature. This example keeps serving cached access and offers a retry when access could not be confirmed at all:
const { hasAccess, isStale, error, refetch } = voidhash.useHasPerk("premium");
if (isStale) {
// Cached access while offline. Usually fine to keep serving content.
}
if (!hasAccess && error) {
return <RetryScreen onRetry={refetch} />;
}
return hasAccess ? <PremiumContent /> : <UpgradePrompt />;Sometimes you need fresh confirmation, for example before unlocking a consumable. Pass
{ allowStale: false } to the imperative check so it never answers from cache:
const result = await voidhash.client.hasPerk("premium", { allowStale: false });
if (result.isErr()) {
// Could not confirm access right now.
}Read the current person
When you need more than a single perk check, read the full snapshot:
const { data: person, error, isLoading, refetch } = voidhash.useCurrentPerson();data is the person snapshot. It is null until the first snapshot loads, and while the client is
disabled. See the SDK reference for the full shape.
For advanced cases you can still filter the grants directly. Prefer useHasPerk for gating:
const premiumGrant = person?.entitlements.grants.find(
(grant) => grant.perkId === "premium" && grant.status === "active",
);Grant fields
Each entry in entitlements.grants has these fields.
| Field | Meaning |
|---|---|
perkId | The perk slug configured in Studio. |
status | active or expired. |
source | subscription, purchase, or manual. |
sourceId | The subscription, purchase, or manual grant that created it. |
expiresAt | Expiration time, or null for access without an expiry. |
Use subscriptions.current for account UI, such as the current plan or its renewal state. Use
purchases.history when you need to show past transactions.
Refresh behavior
The SDK refreshes the person after purchases, restores, and identity changes. Between refreshes, reads use a stale-while-revalidate cache: a snapshot younger than five minutes is returned immediately while a fresh copy is fetched in the background. An older snapshot triggers a network fetch first. When you need a network round trip right away, force one:
await voidhash.client.getCurrentPerson({ forceFetch: true });
// Or re-run the hook's request:
refetch();