Permissions & principals
Electric Agents servers authorize requests using a principal and permission grants. A principal identifies the caller; grants decide what that caller can do to entity types and entity instances.
Principals
Pass a principal key as the Electric-Principal header. The key shape is:
<kind>:<id>Supported principal kinds are user, agent, service, and system. The server turns a key such as user:sam into the canonical principal URL /principal/user%3Asam.
From clients, use principalKey:
import { createRuntimeServerClient } from "@electric-ax/agents-runtime"
const client = createRuntimeServerClient({
baseUrl: "http://localhost:4437",
principalKey: "user:sam",
})The CLI can pass the same value through the environment:
ELECTRIC_AGENTS_PRINCIPAL=user:sam electric agents psServers may also accept additional auth headers through ELECTRIC_AGENTS_SERVER_HEADERS or serverHeaders, depending on the host.
Entity type permissions
Entity type grants control who can spawn or manage entities of a type. Type-level permissions are:
| Permission | Allows |
|---|---|
spawn | Spawn entities of this type |
manage | Manage the entity type and acts as the broader type-level permission |
Declare initial type grants in an entity definition:
registry.define("worker", {
description: "Internal worker",
permissionGrants: [
{
subject_kind: "principal_kind",
subject_value: "user",
permission: "spawn",
},
],
async handler(ctx) {
// ...
},
})subject_kind can be principal for one principal URL/key or principal_kind for every principal of a kind.
Built-in Horton and Worker registrations grant all user principals both spawn and manage on those entity types. This makes local and hosted user principals able to create built-in sessions and see the type metadata needed by creation UIs. Custom entity types should choose their own permissionGrants.
Entity permissions
Entity grants control access to existing entities. Entity-level permissions are:
| Permission | Allows |
|---|---|
read | Read entity metadata and streams |
write | Send messages and write entity-owned resources |
delete | Delete or kill the entity |
signal | Send lifecycle signals |
fork | Fork from entity history |
schedule | Create, update, or delete schedules |
spawn | Spawn children from this entity |
manage | Manage grants and acts as the broader entity-level permission |
Server spawn routes can include initial entity grants:
await fetch("http://localhost:4437/_electric/entities/assistant/support-ticket-42", {
method: "PUT",
headers: {
"content-type": "application/json",
"electric-principal": "user:sam",
},
body: JSON.stringify({
grants: [
{
subject_kind: "principal",
subject_value: "/principal/user%3Asam",
permission: "read",
},
],
}),
})When spawning from a parent, broad delegation requires manage on the parent. This applies to grants such as manage, principal-kind grants, descendant propagation, and copy_to_children.
Sharing roles
The server stores granular grants, but the UI presents common sharing roles:
| Role | Grants |
|---|---|
view | read, fork |
chat | read, write, signal, fork, schedule, spawn |
manage | manage, delete |
These are presets for entity grants. They do not change the underlying permission model, and deployments can still grant individual permissions directly.
The server also exposes principal and effective-permission data through Electric shapes for sharing UIs. Point authorization still uses server-side permission checks; Electric visibility is derived from materialized effective permissions.
Grant propagation
Entity grants may include propagation options:
{
subject_kind: "principal",
subject_value: "/principal/user%3Asam",
permission: "read",
propagation: "descendants",
copy_to_children: true,
}propagation: "self"applies to the entity itself.propagation: "descendants"applies through descendant entities.copy_to_children: truecopies the grant when children are spawned.expires_atcan set a grant expiry timestamp.
Claim-scoped write tokens
Some low-level writes are protected by claim-scoped write tokens. Handler APIs such as ctx.setTag() and ctx.deleteTag() already have the active claim context. External clients should usually send messages instead of directly mutating entity-owned state.
If a host reserves the Authorization header for server auth, configure write token transport with writeTokenHeader or claimTokenHeader:
const client = createRuntimeServerClient({
baseUrl: "http://localhost:4437",
headers: { authorization: `Bearer ${serverToken}` },
writeTokenHeader: "electric-claim-token",
})Development fallback
Local development servers can use a development principal fallback. Production deployments should authenticate requests and provide an explicit Electric-Principal header for every request.