React Native
permdock/react-native adds a persisted snapshot so Expo Router Stack.Protected and Tabs.Protected guards answer synchronously on the first frame and revalidate in the background.
Status: planned Phase: 2
Purpose
Expo Router's Stack.Protected and Tabs.Protected (SDK 53 and later) take a plain boolean guard. They are evaluated synchronously during the first render, before any network request can complete. permdock/react-native re-exports everything from permdock/react and adds one option, storage, so the last known snapshot is read from device storage on launch, guards answer immediately, and a fresh snapshot is fetched and swapped in without a flash. See Expo Router research.
API
import { PermDockProvider, usePermDock, usePermission, Protected } from 'permdock/react-native'
import { permissions } from '@/permissions'
<PermDockProvider
storage={mmkvStorage} // { getItem, setItem, removeItem }, sync or async
endpoint="https://api.example.com/permdock"
snapshotUrl="https://api.example.com/permdock/snapshot"
>
<Stack>
<Stack.Protected guard={usePermission(permissions.admin.access).allowed}>
<Stack.Screen name="admin" />
</Stack.Protected>
</Stack>
</PermDockProvider>| Option or export | Role |
|---|---|
storage | Any object with getItem, setItem, removeItem (MMKV, expo-secure-store, AsyncStorage). Synchronous stores answer on the first frame; asynchronous stores answer on the second frame with status: 'pending' in between. |
snapshotUrl | Where to fetch a fresh snapshot after launch and on refresh(). The request carries the app's session token through the headers or fetch option. |
endpoint | Same batched decision endpoint as React, for closure grants. |
revalidate | 'launch' (default), 'focus' (also on app foreground) or a number of seconds. |
usePermission, usePermDock, Protected | Identical to permdock/react; status gains no new values, stale covers the persisted-but-revalidating case. |
usePermissions, useFilter, useTenant, useMemberships, useRoles, useAssignableRoles, useApproval, useSubject | Identical to permdock/react (UI). useTenant().switchTo persists the chosen tenant next to the snapshot so the app reopens in the same organisation; with tenants: 'all' in the persisted snapshot the switch works offline, otherwise it queues a refresh({ tenant }) for the next connection and reports status: 'pending'. |
Request lifecycle
- Launch. The provider reads the persisted snapshot from
storageand validates it. If valid, the store isreadyandstaleat once: guards evaluate against it synchronously. - Revalidate. A fetch to
snapshotUrlruns in the background. On success the snapshot is validated, written back tostorage, and swapped in through the external store; consumers re-render only if an answer changed. - Check.
usePermissionandProtectedbehave exactly as on the web: portable grants answer locally, closure grants go toendpointin batches. - Sign-out or account switch. The app calls
permdock.clear(); the provider removes the persisted snapshot and drops every cached endpoint answer so no grant from the previous user survives on disk.
If storage is empty (first launch) the store starts in pending; guards receive false, which Expo Router treats as "not allowed" and redirects to the anchor route. Apps that need a splash screen until the first snapshot arrives read usePermDock().status.
Offline behaviour
A device with no connectivity keeps answering from the persisted snapshot. Portable grants (including ownership conditions such as authorId equals subject.id) work fully offline because the condition and the resource are both on the device. Closure grants cannot be answered offline: the hook reports server-only and allowed: false, and Protected renders fallback. Apps that need a specific permission offline should express it with a portable condition rather than a closure; permdock doctor lists closure grants that are referenced from React Native code.
Because the on-device answer is a UI hint, an API request made while offline and replayed later is still checked by the server with the current policy, so a stale local grant can never turn into a successful write.
What it validates
- The persisted snapshot against the snapshot v1 schema before use. A snapshot written by an older app version that fails validation is discarded and refetched rather than trusted.
- The snapshot's
subject.idagainst the currently signed-in user id when the app provides one throughsubjectId; a mismatch is treated as empty storage. - Nothing else client-side. As on the web, on-device answers are UI hints; every mutation is re-checked by the API.
How denials surface
- Guards receive
false; Expo Router redirects to the nearest allowed route. Because the answer comes from storage there is no denied-then-allowed flash after launch. Protectedrendersfallback, and thedecisionis available for an "approval required" screen.- Persisted grants are stale by design until revalidation finishes. A role downgrade takes effect on the next successful fetch; for immediate revocation the API should reject the mutation and the app can call
refresh(), or a Shared Signals receiver on the server can rotate the snapshot.
Example app
apps/examples/expo: an Expo Router app with Stack.Protected for an admin group and Tabs.Protected for a billing tab, MMKV storage, a Hono API from apps/examples/hono serving the snapshot and decision endpoint, and a Maestro or Detox flow asserting that the admin route is available on the first frame after a cold start for an admin user and never renders for a member.
Related standards
- AuthZEN: decision endpoint request and response shapes.
- Concepts: snapshots, decisions, tenancy, UI.
- Research: Expo Router.
Open questions
- Whether to encrypt the persisted snapshot by default (grants reveal role names and condition shapes, not data; with
tenants: 'all'it also lists every organisation the user belongs to), or leave the choice to thestorageimplementation. - Whether a persisted multi-tenant snapshot should drop memberships whose
expiresAthas passed at read time (current: yes, the evaluator ignores them anyway) and whether a stale persisted snapshot should keep serving another tenant's grants offline after a server-side removal (current: untilrevalidateruns; the threat model accepts this for UI hints because every mutation is re-checked server-side). - The right default for
revalidate: launch only, or launch plus foreground. - Whether to ship an
expo-secure-storeand an MMKV adapter in the entry or document the three-method interface only. - How
Tabs.Protectedshould behave whilestatusispendingon a cold start with empty storage: hide the tab or show it disabled.
React
permdock/react gives client components a snapshot-backed PermDock through PermDockProvider, usePermDock, usePermission, usePermissions, useFilter, useTenant, useMemberships, useRoles, useAssignableRoles, useApproval, useSubject and Protected, with a batched decision endpoint for closure grants.
Vue
permdock/vue maps the snapshot-backed provider, hook and guard from the React adapter onto a Vue plugin, composables and a component.