Skip to main content

Handle Identity, Caching, And Rebinding

Fluent handles (RealmHandle, ClientHandle, ClientScopeHandle, RoleHandle, UserHandle, ...) carry two pieces of state:

  1. Routing identity — the realm name, client id, role name, scope name, user name, alias, etc. that the handle targets.
  2. Cached representation — the most recently resolved upsert-able representation (client, role, group, user, flow, workflow, organization, identityProvider, component, ...).

Both are read-only from outside the handle. This prevents callers from inadvertently redirecting a handle by mutating its identity, or from poisoning the cached representation in front of a destructive call. The only supported way to retarget a handle after construction is the public rebind(newId) method described below.

What This Means For You

Reads are unaffected

All the public read access patterns keep working:

const role = await realm.role('manage-users').ensure({});

role.realmName; // 'demo'
role.roleName; // 'manage-users'
role.role; // the resolved RoleRepresentation (or null/undefined before get())
role.core; // the underlying KeycloakAdminClient (also reachable via kc.core)
role.realmHandle; // the parent RealmHandle

Writes that previously worked now fail at compile time

// These all fail to compile after the encapsulation contract landed:
role.roleName = 'other'; // readonly getter
role.role = undefined; // readonly getter
client.clientId = 'other'; // readonly getter
client.client = undefined; // readonly getter
realm.realmName = 'other'; // readonly getter
realm.realm = undefined; // readonly getter

kc.core and per-handle .core / .realmHandle remain publicly readable (documented as such in the Client Fluent API) and are declared readonly so they cannot be redirected after construction.

Re-Targeting A Handle: rebind(newId)

Every parent handle exposes a rebind(newId) method that:

  • Updates the handle's routing identity to the new value.
  • Clears the cached representation so the next read resolves against the new target.
  • Returns this for chaining.
const clientHandle = realm.client('app-a');

await clientHandle.ensure({ description: 'A' });

// Re-target the SAME handle at 'app-b'. Subsequent operations resolve B,
// never a stale snapshot of A.
clientHandle.rebind('app-b');
await clientHandle.ensure({ description: 'B' });

Child handles follow the parent automatically

Per the parent-as-source-of-truth contract (HANDLE-01), child handles read the parent's identity live. Rebinding a parent re-targets every existing child on the child's next operation — there is no separate "invalidate" call you need to make.

const clientHandle = realm.client('app-a');
const roleHandle = clientHandle.role('reader');

await clientHandle.get(); // resolves app-a
await roleHandle.get(); // resolves the reader role on app-a

clientHandle.rebind('app-b');
roleHandle.rebind(roleHandle.roleName); // clear child cache; same role name

await roleHandle.get(); // now resolves the reader role on app-b

The same pattern applies to client-scope protocol mappers (rebind on the parent ClientScopeHandle, then rebind on the child mapper). Child handles without a meaningful identity of their own (group children) still expose rebind(newGroupName) to clear the cached group representation while changing the local group name.

Which Handles Support rebind?

Handlerebind(newId) parameter
RealmHandlenew realm name
ClientHandle (and confidential/public/service-account subclasses)new client id
ClientScopeHandlenew scope name
RoleHandlenew role name
ClientRoleHandlenew role name (parent client is followed live)
UserHandlenew username
OrganizationHandlenew organization alias
IdentityProviderHandlenew alias
IdentityProviderMapperHandlenew mapper name
AuthenticationFlowHandlenew alias
WorkflowHandlenew workflow name
ComponentHandlenew component name
GroupHandle / ChildGroupHandle / NestedChildGroupHandlenew group name
ProtocolMapperHandlenew mapper name
ClientScopeProtocolMapperHandlenew mapper name
AttackDetectionHandlenew user id

CacheHandle and ClientPoliciesHandle have no per-handle identity beyond the realm, so they do not expose rebind; use RealmHandle.rebind to retarget their realm.

Contract Stability

Re-targeting through rebind is the only intentional way to reuse a handle across identities. The fields previously exposed as public mutable (clientId, scopeName, roleName, username, alias, ... and the matching cached representations) are now backed by private storage accessed through public readonly getters. This is a TypeScript compile-time contract change — runtime code that read these fields continues to work, and the rebind() API replaces direct field mutation. The compile fixture in tests/implementation-handle-visibility.spec.ts enforces the visibility contract on every CI run so the encapsulation cannot regress silently.