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​

Child handles derive their parent routing identity from a shared identity version contract. Rebinding a parent bumps that version, so existing children notice the parent-generation change, clear any cached representation from the old parent, and re-resolve on their next operation. There is no separate "invalidate" call and no child rebind() 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');

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

The same transitive behavior applies across multiple levels, including realm children, client roles, client protocol mappers, client-scope protocol mappers, identity-provider mappers, and nested groups. For example, rebinding a ClientScopeHandle automatically invalidates an already-resolved protocol mapper under that scope, and rebinding a parent group changes the live path used by existing child-group handles. Realm-owned cached descendants such as UserStorageProviderHandle.providerName also clear cached values after a realm rebind before their next operation or cache read.

If neither the local identity nor any parent identity changed, handles keep their cached parent representation and preserve the no-duplicate-lookup fast path.

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

UserStorageProviderHandle is keyed by its provider ID and does not expose a local rebind() method; when its parent realm is rebound, it follows the new realm and invalidates its cached provider name automatically.

CacheHandle and ClientPoliciesHandle have no local cached representation or 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.