Thin, fail-closed React Native client and hooks for the Laravel IAM control plane.
Ask the IAM server "can this user do this?" from your React Native (or React) app — with the exact same wire contract and guarantees as the PHP and Node clients, plus React hooks that stay fail-closed while loading.
📚 Full documentation: doc.laravel-iam-react-native.padosoft.com — quickstart, hook lifecycle, fail-closed theory, the Hermes/Web Crypto caveat, ADRs, and the full API reference.
- Fail-closed by construction. Network error, timeout, 5xx, 4xx, malformed body, or unverifiable token resolves to deny. Always. Loading resolves to deny. No fail-open switch.
- No PDP logic client-side. Every verdict comes from the server's
decisions/check. The client never interprets grants. - React Native safe. No
node:crypto, no Node built-ins. Uses the RN fetch polyfill andjose(Web Crypto /globalThis.crypto.subtle). - Zero runtime coupling to the Node SDK. Wire types are imported from
@padosoft/laravel-iam-nodewithimport type— completely erased at build time. No Node SDK runtime code runs in your app. - First-class React hooks.
usePermissionanduseCanintegrate with React context — check a permission in one line.
npm install @padosoft/laravel-iam-react-nativeRequires React Native >= 0.71 (Hermes with
globalThis.crypto.subtle) forverifyToken. Forcheck()/ hooks only, any RN version withfetchworks.
import { IamClient, IamProvider } from '@padosoft/laravel-iam-react-native';
const iam = new IamClient({
baseUrl: 'https://iam.example.com/api/iam/v1',
token: process.env.IAM_SERVICE_TOKEN,
timeoutMs: 2000,
cache: { ttlMs: 5000 },
});
export default function App() {
const userId = useAuth().userId;
return (
<IamProvider client={iam} subject={{ type: 'user', id: userId }}>
<Navigation />
</IamProvider>
);
}import { usePermission } from '@padosoft/laravel-iam-react-native';
function StockAdjustButton({ warehouseId }: { warehouseId: string }) {
const { allowed, loading } = usePermission(
'stock.adjust',
{ type: 'warehouse', id: warehouseId },
);
if (loading) return <ActivityIndicator />;
if (!allowed) return null;
return <Button title="Adjust stock" onPress={handleAdjust} />;
}import { IamClient } from '@padosoft/laravel-iam-react-native';
const decision = await iam.check({
subject: { type: 'user', id: 'usr_123' },
application: 'warehouse',
permission: 'stock.adjust',
resource: { type: 'warehouse', id: 'wh_milan' },
context: { amount: 300 },
});
if (!decision.allowed) throw new Error('Forbidden');
if (decision.requiresStepUp) promptStepUp(decision.requiredAal);allowed === true alone is not permission. When requiresStepUp is true, the action is only permitted at a higher AAL. The hooks apply isGranted() automatically — they only set allowed: true when the PDP allowed and no step-up is pending.
| Prop | Type | Description |
|---|---|---|
client |
IamClient |
Pre-configured client instance. |
subject |
Subject |
The authenticated user. Hooks use it automatically. |
children |
ReactNode |
Your app tree. |
Returns { client, subject } from the nearest IamProvider. Throws if no provider is found.
Full-control hook — accepts a complete DecisionQuery.
const { allowed, loading, requiresStepUp } = useCan({
subject: { type: 'user', id: userId },
permission: 'doc.publish',
resource: { type: 'document', id: docId },
});Convenience hook — reads subject from context, you supply permission and optionally resource.
const { allowed } = usePermission('orders.approve', { type: 'order', id: orderId });| Field | Type | Description |
|---|---|---|
allowed |
boolean |
true only when PDP granted AND no step-up pending. false while loading. |
loading |
boolean |
true while the check is in flight. |
requiresStepUp |
boolean |
true if a higher AAL is required. |
| Option | Default | Description |
|---|---|---|
baseUrl |
required | Full API base, e.g. https://iam.example.com/api/iam/v1. |
token |
— | The user's access token, sent as Authorization: Bearer. |
timeoutMs |
2000 |
Per-request timeout in ms. |
retries |
0 |
Retries for idempotent network errors (never on 4xx/5xx). |
cache |
off | { ttlMs, maxEntries? } in-memory decision cache. |
verify |
— | { issuer?, audience?, jwksUri? } defaults for verifyToken. |
fetch |
globalThis.fetch |
Inject a custom fetch (tests, proxies). |
Unlike the server SDKs (laravel-iam-client, laravel-iam-node, laravel-iam-rust), a mobile app is a
public OAuth client: it must never embed a client_secret (anything shipped in an app binary is
extractable). So the client-secret rotation / self-fetch feature does not apply here — there is no
secret to rotate.
Instead, obtain the token through the Authorization Code + PKCE flow (the user logs in against IAM),
keep it short-lived, and refresh it with the refresh token — never with a static secret. Pass the
current user access token as token; when it expires, run the refresh/re-auth flow and update the client.
See Application credentials & lifecycle
(§ public clients).
POST {baseUrl}/decisions/check. Returns a normalised Decision. Never throws.
check() reduced to the fail-safe boolean.
ReBAC list-resources. Returns [] on any error.
Verifies an ES256 token against the server JWKS. Rejects with TokenVerificationError on any failure.
This package consumes decisions (it asks "can this subject do X?"); it does not own a permission catalog and does not declare or push manifests. A mobile/public client has no server-side catalog to sync. Permissions and roles are declared by the service that owns them (a Laravel app with the bridge, or a Node/Rust service, or the console) and synced there. See Keeping IAM in sync.
| Package | Runtime | Description | Docs |
|---|---|---|---|
laravel-iam-server |
PHP/Laravel | The IAM server — the PDP itself (RBAC + ABAC + ReBAC, OAuth/OIDC, audit) | docs |
@padosoft/laravel-iam-node |
Node 18+ | Core TypeScript/Node SDK — this package builds on its wire types | docs |
@padosoft/laravel-iam-react-native |
React Native / React | This package | docs |
padosoft/laravel-iam-client |
PHP 8.1+ | PHP client — the wire-contract reference | docs |
laravel-iam-rust |
Rust | Rust client SDK (crate laravel-iam), async + blocking |
docs |
MIT (c) Padosoft