Identity
The React implementation of identity is built on top of what you find the core. It provides an encapsulation that feels more natural to a React application.
HTTP Headers
Section titled “HTTP Headers”Identity fetches include httpHeadersCallback headers configured through Arc. Use this for Authorization or application headers, not a browser Cookie header. Cookies are browser-managed. The identity comes from /.cratis/me, which derives it from the authenticated request. In the browser it is UI data that the user can change; authorization belongs on the server.
Identity provider context
Section titled “Identity provider context”<Arc> already supplies an identity provider. Use a separate provider only for deliberate configuration, such as a generated details constructor; render it beneath Arc and explicitly preserve any headers you need. Identity request settings are global, so nested providers are not independent authenticated sessions.
For the default identity shape, use the provider already included in your top-level Arc:
import { Arc } from '@cratis/arc.react';
export const App = () => ( <Arc> <main>Your application content</main> </Arc>);This context can then be used anywhere by consuming the React context directly:
import { IdentityProviderContext } from '@cratis/arc.react/identity';
export const SomeComponent = () => { return ( <IdentityProviderContext.Consumer> {({ identity, isLoading }) => ( <h1>{isLoading ? 'Loading…' : identity.name}</h1> )} </IdentityProviderContext.Consumer> );};The raw context shape is
{ identity, detailsConstructor, isLoading, clearIdentity }. Details live atvalue.identity.details, notvalue.details. The default generic details type isobject; prefer a typeduseIdentityconsumer.
Refreshing
Section titled “Refreshing”Sometimes you need to refresh the identity due to backend changes. On the IIdentityContext that represents
the context you find a method called refresh(). Calling this asks
the backend for the current identity details and replaces the identity kept in memory.
import { IdentityProviderContext } from '@cratis/arc.react/identity';
export const SomeComponent = () => { return ( <IdentityProviderContext.Consumer> {({ identity }) => ( <> <h1>{identity.name}</h1> <button onClick={() => void identity.refresh()}>Refresh identity</button> </> )} </IdentityProviderContext.Consumer> );};useIdentity() hook
Section titled “useIdentity() hook”Anywhere within your application you can then access the identity by adding using the useIdentity() hook:
import { useIdentity } from '@cratis/arc.react/identity';
export const Home = () => { const identity = useIdentity();
return ( <h3>User: {identity.name}</h3> );};The useIdentity() hook returns the context which holds a property called details. This details property is what the backend
returned to the ingress middleware.
By default, if not specified, the type of the details is object. You can change this by passing it a generic argument with
the exact shape of what’s expected:
import { useIdentity } from '@cratis/arc.react/identity';
type Identity = { firstName: string; lastName: string;};
export const Home = () => { const identity = useIdentity<Identity>(); if (identity.isLoading) return <p>Loading…</p>; if (!identity.isSet || !identity.details) return <p>No identity details.</p>;
return ( <h3>User: {identity.details.firstName} {identity.details.lastName}</h3> );};The generic describes the expected payload; it does not make optional backend details present. Guard details even after loading finishes, or supply the deliberate defaults shown below.
Has the identity arrived yet?
Section titled “Has the identity arrived yet?”The identity is fetched from the backend, which means there is a moment - short, but real - where your
application is rendering and nobody knows who the user is yet. isSet cannot tell you about that
moment: it reads false both before the first request has answered and after it answered that
nobody is signed in.
That is what isLoading is for. It is on the object useIdentity() returns and it separates the two:
import { useIdentity } from '@cratis/arc.react/identity';
export const Greeting = () => { const identity = useIdentity();
if (identity.isLoading) return <Spinner />; if (!identity.isSet) return <SignInPrompt />;
return <h3>Welcome back, {identity.name}</h3>;};isLoading covers every resolution, not only the first one. Calling refresh() - which is what you do
after a sign-in or after the backend grants a role - puts it back up until the round-trip answers, so
anything that gates on who the user is keeps waiting instead of briefly deciding on the old identity.
Note that isLoading lives on IIdentityContext, not on the framework-agnostic IIdentity in the
core package. It is a React lifecycle concern and it stays on the React side.
Role checking
Section titled “Role checking”The identity context includes information about the roles assigned to the user. You can check if a
user is in a specific role using the isInRole() method, or read the roles array directly:
import { useIdentity } from '@cratis/arc.react/identity';
export const UserProfile = () => { const identity = useIdentity();
return ( <div> <h3>User: {identity.name}</h3> <p>Roles: {identity.roles.join(', ')}</p> {identity.isInRole('Admin') && <AdminBadge />} </div> );};That is the right shape for decorating a screen. It is the wrong shape for guarding one:
// Don't do this - it decides on an identity that may not have arrived.if (!identity.isInRole('Admin')) { return <div>Access denied. Admin role required.</div>;}Before the identity request answers, an administrator holds no roles yet - so this renders “Access denied,” then swaps to the panel a moment later. Every signed-in user sees the rejection flash by on every load. There are three outcomes here, not two, and a guard has to say something about all of them.
Guarding a screen with RequireRole
Section titled “Guarding a screen with RequireRole”RequireRole is that guard. It renders one of three slots, and which one is never in doubt:
import { RequireRole } from '@cratis/arc.react/identity';
export const Admin = () => ( <RequireRole roles={['Administrator', 'Auditor']} whileLoading={<Spinner />} forbidden={<AccessDenied />}> <AdminPanel /> </RequireRole>);| Prop | Type | What it does |
|---|---|---|
roles | string[] | Roles that grant access - the identity needs any one of them |
allow | (details, identity) => boolean | Predicate deciding access from the identity’s details |
children | ReactNode | Rendered when the caller is authenticated and allowed |
whileLoading | ReactNode | Rendered while the identity is still being resolved. Defaults to nothing |
forbidden | ReactNode | Rendered for an anonymous caller, or an authenticated one that is not allowed. Defaults to nothing |
At least one of roles and allow has to be there; the type system says so, and the component denies
at runtime if undefined gets past it anyway. Supply both and both must pass.
forbidden is a slot rather than a redirectTo on purpose. Arc.React does not depend on a router, so
instead of picking one for you it lets you hand it whatever your application already uses:
<RequireRole roles={['Administrator']} forbidden={<Navigate to="/" replace />}> <AdminPanel /></RequireRole>Deciding on details
Section titled “Deciding on details”Roles are what the backend put in the token. When access depends on something your own domain knows, pass a predicate over the identity’s details:
import { RequireRole } from '@cratis/arc.react/identity';
type OrganizationDetails = { department: string;};
export const Ledger = () => ( <RequireRole<OrganizationDetails> allow={details => details?.department === 'Finance'} forbidden={<AccessDenied />}> <LedgerContent /> </RequireRole>);The details come first because that is what an application’s own access rules are written against; the
whole identity arrives as a second argument when a rule also needs isInRole.
The details are typed as possibly absent, and the ?. above is not decoration. An identity is set as
soon as the backend answers, whether or not the application registered anything to fill details in -
so a predicate written as though they are always there throws on exactly the deployments that have
none.
Everything that is not a yes is a no
Section titled “Everything that is not a yes is a no”A gate that cannot reach a decision does not open. RequireRole renders forbidden for all of these,
and warns on the console for the ones that are configuration mistakes:
- The caller is anonymous.
- No role matched, or the predicate answered
false. rolesis an empty array. That is not “no rule” - it is a rule no caller can satisfy.rolesis not an array at all (anullout of JSON configuration, say).- Neither
rolesnorallowwas supplied. A key renamed in a feature-flag object arrives asundefined, compiles, lints, and would otherwise open the panel to everyone signed in. If authentication alone really is the rule, say so:allow={() => true}. - The predicate threw.
- The identity carries no details but an
allowpredicate was supplied. There is nothing to decide on, and the two natural ways to phrase a predicate disagree about absence - one throws, the other reads it as innocence and admits - so the gate answers instead of letting the phrasing decide.
RequireRole hides UI, it does not protect data
Section titled “RequireRole hides UI, it does not protect data”Type-safe identity with complex types
Section titled “Type-safe identity with complex types”If your identity details contain complex types like Guid from @cratis/fundamentals, you can enable type-safe deserialization by giving <Arc> a detailsType constructor. This ensures that complex types are properly instantiated with their methods and behavior, not just plain JSON objects.
First, define your identity details class with an @field decorator on every property that should be deserialized:
import { Guid, field } from '@cratis/fundamentals';
class UserIdentityDetails { @field(Guid) userId!: Guid;
@field(String) firstName!: string;
@field(String) lastName!: string;}Then, configure <Arc> with the details type. This is the primary form - <Arc> forwards it to the IdentityProvider it mounts internally, deserialization happens once, and every useIdentity() call anywhere in the tree receives a real UserIdentityDetails instance at runtime:
import { Arc } from '@cratis/arc.react';
export const App = () => { return ( <Arc detailsType={UserIdentityDetails}> {/* ... your app content ... */} </Arc> );};If you wire IdentityProvider directly instead of using <Arc> - see Identity provider context - it accepts the same detailsType prop.
The runtime instance does not change TypeScript’s view: a bare useIdentity() types details as object. Pass the details type as the generic argument (or the constructor) where you read it:
import { useIdentity } from '@cratis/arc.react/identity';
export const Home = () => { const identity = useIdentity<UserIdentityDetails>(); // identity.details.userId is a proper Guid instance with all its methods return ( <> <h3>User ID: {identity.details.userId.toString()}</h3> <h3>User: {identity.details.firstName} {identity.details.lastName}</h3> </> );};You only need to pass the constructor to useIdentity() itself when the provider was not configured with a detailsType - for example, a shared component that has to work whether or not the hosting application set one up:
import { useIdentity } from '@cratis/arc.react/identity';
export const Home = () => { const identity = useIdentity(UserIdentityDetails);
return ( <h3>User ID: {identity.details.userId.toString()}</h3> <h3>User: {identity.details.firstName} {identity.details.lastName}</h3> );};It is safe to supply both. useIdentity(UserIdentityDetails) recognizes when <Arc>/IdentityProvider already deserialized the payload with that exact type and hands back the existing instance rather than deserializing it a second time - which would be destructive, not merely wasteful, for nested temporal values and concept types.
This approach uses JsonSerializer.deserializeFromInstance() under the hood to recursively deserialize complex types, ensuring that types like Guid, DateOnly, and other @field-decorated custom types are properly instantiated rather than being plain JSON objects.
Refreshing with hook
Section titled “Refreshing with hook”Since the useIdentity() returns an instance of the IIdentityContext. So for refreshing with a hook, its easily
accessible:
import { useIdentity } from '@cratis/arc.react/identity';
type Identity = { firstName: string; lastName: string;};
export const Home = () => { const identity = useIdentity<Identity>();
return ( <> <h3>User: {identity.details?.firstName} {identity.details?.lastName}</h3> <button onClick={() => void identity.refresh()}>Refresh identity</button> </> );};Default value
Section titled “Default value”You can also provide a default value for the details property in the identity context.
Clearing identity
Section titled “Clearing identity”When a user logs out, you can clear the client-side identity state using clearIdentity(). This is available on the object returned by useIdentity():
import { useIdentity } from '@cratis/arc.react/identity';
export const ClearIdentityCacheButton = () => { const identity = useIdentity();
const handleLogout = () => { identity.clearIdentity(); };
return <button onClick={handleLogout}>Clear identity cache</button>;};Calling clearIdentity() does two things:
- Forgets the identity kept in memory by
IdentityProvider, so the next request for it asks/.cratis/meagain. - Resets the identity context to its initial unset state (
isSetbecomesfalse,detailsis reset).
Clearing this cache is not logout: authentication cookies, tokens, and server sessions remain unchanged. Complete real sign-out through your authentication system first, then clear identity and reconnect queries. Reconnection uses current browser credentials and does not purge cached query data.
Default details value
Section titled “Default details value”The provider starts with an empty details object, {}, while identity is unset. A resolved identity can still carry null or missing details; there is no unconditional empty-object fallback. A loading check alone does not make details safe to dereference.
Supply a typed default for display placeholders. It is used while identity is unset and when resolved details are absent; it does not fill individual missing fields or establish authentication.
The default value can be provided as an argument to the useIdentity() hook:
import { useIdentity } from '@cratis/arc.react/identity';
type Identity = { firstName: string; lastName: string;};
export const Home = () => { const identity = useIdentity<Identity>({ firstName: '[N/A]', lastName: '[N/A]' });
return ( <h3>User: {identity.details.firstName} {identity.details.lastName}</h3> );};When using the type-safe overload with a constructor, the default value is provided as the second parameter:
import { useIdentity } from '@cratis/arc.react/identity';import { Guid, field } from '@cratis/fundamentals';
class UserIdentityDetails { @field(Guid) userId!: Guid;
@field(String) firstName!: string;
@field(String) lastName!: string;}export const Home = () => { const defaultDetails: UserIdentityDetails = { userId: Guid.empty, firstName: '[N/A]', lastName: '[N/A]' };
const identity = useIdentity(UserIdentityDetails, defaultDetails);
return ( <h3>User: {identity.details.firstName} {identity.details.lastName}</h3> );};