The SDK persists CustomerInfo locally and publishes it on startup before
network sync completes, so your app renders access state immediately. Offline
behavior is deliberately conservative: it neither erases valid access on a
network blip nor extends access forever.
Cache scoping
The default AsyncStorage cache is scoped to the SDK key, installation ID, store environment, and a hashed app-user identity. Switching users cannot read another user’s cached access.
Freshness states
customerInfo.freshness reports how current the data is:
| Freshness | Meaning |
|---|---|
fresh |
Recently confirmed by the server |
stale |
Cache older than customerInfoStaleAfterMs (default 24 h) — still usable |
syncing |
A refresh is in flight |
offline |
Last refresh failed; serving cached data |
error |
Refresh failed with no usable cache |
Staleness itself does not revoke access; it is a signal that a refresh is due.
Offline expiry rules
Cached entitlements are re-evaluated whenever they are read:
- Expiring entitlements stay active offline only until their
server-provided
expiresAt. Past that, they flip toactive: false, status: 'expired'locally. - Non-expiring entitlements (lifetime, some contracts) stay usable offline
for at most
nonExpiringEntitlementMaxOfflineAgeMsafter theirverifiedAt(default 30 days). A device that never reconnects cannot hold lifetime access forever. - An expired
accessContextis removed from cached/offline customer info even when the entitlement itself remains usable within its offline window.
Device Access expiry
When a Device Activation policy applies, offline installation access is bounded by the server-issued Device Access expiry in addition to entitlement expiry. The effective local boundary is the earliest known applicable expiry. Replacement or revocation invalidates online authorization immediately; an already offline old installation cannot be recalled before the opaque token/cache expiry that was previously issued.
SubKit does not promise exact concurrent-session enforcement and does not require a background heartbeat. Renewal is attempted during normal foreground, purchase, restore, identity, and explicit refresh flows. Mobile operating systems may suspend the app indefinitely in the background.
Client-clock rollback detection can be used only as a best-effort diagnostic signal. It is not a security guarantee and must not silently extend an expired server-issued authorization.
Failure semantics
A failed refresh with valid cached data returns a normal access decision with
evidence.freshness === 'offline'. It does not clear previously granted
access before known expiry. Without usable cached evidence, the lifecycle state
is offline_unavailable and access stays fail-closed.
import { useSubKitAccess } from '@piparotech/subkit-expo'
const access = useSubKitAccess('pro')
if (access.state === 'granted' && access.evidence.freshness === 'offline') {
// bounded cached access is still valid; optionally show a connectivity note
}
if (access.state === 'offline_unavailable') {
// no bounded decision exists — keep the feature locked and offer retry
}What accessContext is
Fresh CustomerInfo includes a short-lived, signed, opaque accessContext
after provider verification. A trusted app backend can forward it to SubKit
server reads: the token binds app, app user, and the provider-verified store
environment, and expires independently of offline entitlement visibility. The
mobile app treats it as opaque — never parse or persist it separately. Device management and Device Access tokens are separate capabilities and must not be folded into this token or stored as ordinary CustomerInfo fields.
Tuning
Apps with stricter requirements can lower customerInfoStaleAfterMs and
nonExpiringEntitlementMaxOfflineAgeMs, or provide their own
customerInfoCache implementation — see
Advanced configuration.