---
title: "Offline access"
description: "How cached CustomerInfo behaves offline — freshness states, expiry rules, and the accessContext lifecycle."
---

# Offline access

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 to
  `active: false, status: 'expired'` locally.
- **Non-expiring entitlements** (lifetime, some contracts) stay usable offline
  for at most `nonExpiringEntitlementMaxOfflineAgeMs` after their `verifiedAt`
  (default 30 days). A device that never reconnects cannot hold lifetime
  access forever.
- An expired **`accessContext`** is 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.

```tsx compile
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](/docs/expo/advanced/#custom-customerinfo-cache).

## Next

- [Ownership & unclaimed](/docs/expo/conflicts/)

Source: https://subkit.piparo.tech/expo/offline/index.mdx
