# Reference

---

# The JSON API

_Universal Directory / Reference_

> Endpoint-by-endpoint reference for udd's JSON API: object CRUD, rename/move, attribute writes with per-value metadata, path resolution, the schema summary, and every error code.

Everything [`udd`](/universal-directory/getting-started/running-udd.md) can do is exposed as JSON under `/api`. The [debug console](/universal-directory/the-console/the-debug-console.md) is a client of this API and nothing more.

Requests and responses are `application/json`. **There is no authentication**: anyone who can reach the port has full control of the directory.

## Object payload

Endpoints that return an object use one shape:

```json
{
  "id": "9f2c…",                 // GUID (identity)
  "parent": "a01b…",             // GUID or null (root only)
  "name": "Jack",
  "class": "user",
  "classId": "c3d4…",
  "system": false,
  "facets": ["described", "person", "securityPrincipal"],   // effective surface
  "dn": "CN=Jack,OU=Sales",      // derived
  "path": "/Sales/Jack",         // derived
  "hasChildren": false,
  "stage": "alive",              // stored lifecycle stage (alive/deleted/shredded)
  "effectiveStage": "alive",     // stage after apply-time aging
  "ghost": false,                // class definition not yet in this node's schema
  "existence": { …stamp… },      // the existence atom's stamp (creation + stage transitions)
  "placement": { …stamp… },      // the (parent, name) atom's stamp
  "usnChanged": 51,              // node-private change-scan index
  "referencedBy": [              // the back-reference index: who points here
    { "id": "77aa…", "path": "/Admins", "attribute": "member" }
  ],
  "attributes": [
    { "name": "member",
      "values": [
        { "value": "b7e1…",
          "meta": { "expiry": ["2027-01-01T00:00:00Z"] },   // only when present
          "refPath": "/Sales/Jack",                         // reference syntax only; null unless target alive
          "refState": "alive",                              // alive / deleted / shredded / missing
          "stamp": { …stamp… } }                            // this value atom's stamp
      ],
      "absent": [                                           // only when present
        { "key": "c9a2…", "stamp": { …stamp… } }            // stamped residue of a removed value
      ] }
  ]
}
```

`refPath` appears only on values of reference-syntax attributes: the target's current native path, or `null` if the reference dangles.

Every merge atom — existence, placement, each attribute value — carries a **stamp**, the [replication record](/universal-directory/concepts/objects-and-identity.md) that decides merges:

```json
{
  "version": 2,                          // bumped per originating write to this atom
  "at": "2026-08-14T04:01:12Z",          // originating wall-clock time
  "origin": "e8a2…",                     // originating invocation id
  "cert": { "invocation": "e8a2…", "usn": 51 },   // transfer certificate (enumeration coordinate)
  "localUsn": 51                         // node-private
}
```

`absent` lists **absence atoms**: values (or, for a single-valued attribute, the whole attribute — `key: null`) that were removed and persist as stamped residue so the removal can win future merges. Rewriting an attribute with an unchanged value does not restamp it; multi-valued value sets are unordered and render in canonical (set-key) order.

Objects under a **derived repair** additionally carry `conflicted` / `orphaned` / `cycleBroken` flags, plus `effectiveName` (the CNF display name) and `effectiveParent` where they differ from the stored facts; `dn` and `path` always render the effective view. Child summaries carry `name` (effective), `ghost`, and the three repair flags.

## Endpoints

| Method & path | Purpose |
|---|---|
| `GET /api/node` | This node's replication identity and sync state: `{nodeId, invocationId, usn, hasRoot, purgeHorizon, morgueSize, rollbackLatched, erasureDebt, storage, boot, machine, utd, watermarks}` — `storage` carries the [durability marks](/universal-directory/concepts/persistence.md) (generation, durable vs appended bytes and USNs, dead flag), `boot` the boot report, `machine` this machine's identity (`{object, machineKey}` — the machine object's GUID and public key). |
| `GET /api/journal` | The node's observability journal (newest last, capped, survives restarts): `{at, kind, detail}` — merges that discarded concurrent input, refused records and pulls, derived repairs, rollback events. |
| `GET /api/storage` | The [storage view](/universal-directory/concepts/persistence.md): generation files with sizes, durability marks, rotation threshold, erasure debt, background-rebuild status and last outcome. |
| `GET /api/fabric` | The fabric view: this machine's identity and GIP bind, every discovered peer with its link state (`idle`/`dialing`/`connected`/`viaInbound`/`backoff`), live QUIC stats (RTT, congestion window, lost packets, MTU, wire bytes), inbound connections (each carrying `machine {guid, name}` once identified via `gip/ident`, null while unverified), **`channels`** (live channels: connection, peer, service, direction, age, bytes) and **`recentChannels`** (a ring of finished channels and failed opens with duration, bytes, and outcome), the supervisor's config, and a ring of timestamped fabric events. |
| `POST /api/fabric/redial/{guid}` | Debug: clear a peer's backoff and dial it now. |
| `GET /api/msgs` | The machine-talk log, oldest first (RAM only — capped at 200 records, gone on restart): each record is `{atMs, direction (in/out), peerGuid, peerName, text, outcome}`, where sent records carry `outcome` `delivered` (+`rttMs`), `failed` (+`detail`), or `unconfirmed` (+`detail`), and received records carry `received`. `{enabled: false}` on a node without a fabric. |
| `POST /api/msgs/send` | Send one `ud/msgs` message and block for its outcome (bounded: 5 s channel-open + 10 s ack). Body: `{"to", "text"}` — `to` is a machine GUID or a caseless machine name resolved against the fabric's targets; `text` is 1–16384 bytes. HTTP errors are for request problems only (`422 unknown-recipient`, `422 bad-text`, `503 no-fabric`); a send that ran returns `200` carrying the finished record, **including** `failed` and `unconfirmed` outcomes — those are data, not transport errors. |
| `POST /api/storage/rotate` | Debug: seal the active WAL, open the next generation, kick a background rebuild. |
| `POST /api/storage/rebuild` | Debug: kick a background snapshot rebuild (no rotation). |
| `GET /api/root` | The root object. |
| `GET /api/morgue` | Every tombstone: `{id, name, class, stage, effectiveStage, at, lastPath}`. |
| `POST /api/gc` | Run local GC now (harden aged tombstones, purge expired ones); returns the new horizon. |
| `POST /api/objects/{id}/restore` | Restore a soft-deleted object (top-down; refuses conflicts and elapsed windows). |
| `POST /api/objects/{id}/shred` | Security-erase (subtree-wide from alive, or escalate an existing soft delete). |
| `GET /api/objects/{id}` | One object by GUID. |
| `GET /api/objects/{id}/children` | Child summaries, in caseless name order: `{id, name, class, system, hasChildren}`. |
| `POST /api/objects/{id}/children` | Create a child. Body: `{"name", "class", "attributes"?}` — `attributes` maps attribute name → value list and is applied atomically with the create ([definitions are born whole](/universal-directory/concepts/modifying-the-schema.md)). Returns `201` + the object. |
| `PATCH /api/objects/{id}` | Rename and/or move (ModifyDN shape). Body: `{"name"?, "parent"?}`, `parent` by GUID. |
| `PUT /api/objects/{id}/attributes/{name}` | Replace an attribute's values wholesale. Body: `{"values": [{"value", "meta"?}]}`. An empty `values` deletes the attribute. Returns the updated object. |
| `DELETE /api/objects/{id}` | Soft-delete the object and its whole subtree (restorable tombstones). Returns `204`. |
| `GET /api/path` and `GET /api/path/{path}` | Resolve a native untyped path (caseless) to its object: `/api/path/Sales/Jack`. Bare `/api/path` is the root. |
| `GET /api/schema` | The compiled schema summary (below). |

Attribute names in URLs and bodies are caseless and accept wire names (`Acme-badgeColor`); the stored spelling is always the schema's canonical one.

Every mutating endpoint acknowledges only after the write is [durable](/universal-directory/concepts/persistence.md) — a `200`/`201`/`204` means the disk holds it. Attribute values are capped at 1 MiB each (large blobs are a future side-store's job, not an atom's).

## The schema summary

`GET /api/schema` returns the compiled schema, sorted by name:

```json
{
  "attributes": [ { "id", "name", "syntax", "singleValued", "matching", "aliases", "legalMeta" } ],
  "facets":     [ { "id", "name", "attributes": [ { "name", "required" } ] } ],
  "classes":    [ { "id", "name", "dnPrefix", "instantiable", "facets" } ],
  "extensions": [ { "id", "name", "targetClass", "facets" } ],
  "quarantined": [ { "id", "name", "reason" } ]
}
```

`quarantined` lists definitions excluded from the effective schema — replication can deliver a facet before its attributes. Compilation is *total*: it never fails, it quarantines what cannot join yet (with the reason) and lets it join automatically when its dependencies arrive. References to *tombstoned* definitions compile out instead (the facet keeps working minus its dead member). Originating schema writes are still strictly gated: a local write that would grow the quarantine is refused with `schema-violation`.

## Errors

Failures return `{"error": "<code>", "message": "<human text>"}` with:

| Code | Status | Meaning |
|---|---|---|
| `bad-object-id` | 400 | Path segment is not a GUID. |
| `no-such-object` | 404 | Object (or path/reference target) does not exist. |
| `no-root` | 503 | A joined store that has not yet received its first sync has no root. |
| `name-in-use` | 409 | Sibling name taken (caseless, type-blind). |
| `invalid-name` | 422 | Name fails [validity rules](/universal-directory/reference/naming-rules.md) (or definition-name grammar). |
| `unknown-class` | 422 | No class by that name. |
| `class-not-instantiable` | 422 | Class is marked non-instantiable (`domainRoot`, `configuration`). |
| `unknown-attribute` | 422 | No attribute definition by that name. |
| `attribute-not-permitted` | 422 | Attribute is outside the object's surface. |
| `single-valued` | 422 | Multiple values for a single-valued attribute (or meta). |
| `syntax-violation` | 422 | Value fails its syntax (bad integer/boolean/timestamp, non-GUID or dangling reference). |
| `illegal-meta-key` | 422 | Meta key unknown or not in the attribute's `legalMeta`. |
| `duplicate-value` | 409 | Value set contains duplicates under the matching rule. |
| `unknown-facet` | 422 | `attachedFacets` value is not a facet definition. |
| `facet-has-values` | 409 | Facet detach / extension delete refused while exclusively-provided attributes hold values. |
| `already-deleted` | 409 | Mutation aimed at a tombstone (restore first, or leave the dead be). |
| `ghost-object` | 409 | The object's class isn't in this node's schema yet; originating writes refuse until sync converges. |
| `rollback-quarantine` | 409 | The node detected it is running rolled-back (restored) state: its invocation was re-minted and originating writes are suspended until a completed pull heals it. |
| `not-restorable` | 409 | Restore refused: wrong stage, elapsed window, or parent not alive. |
| `root-is-protected` | 409 | The root cannot be deleted, renamed, or moved. |
| `system-object-protected` | 409 | System objects refuse moves and deletion (their attributes are writable). |
| `invalid-schema-placement` | 422 | Wrong class/parent combination in or around the schema subtree. |
| `schema-definition-immutable` | 409 | Definitions are add/delete only. |
| `definition-in-use` | 409 | [Unused-delete rule](/universal-directory/concepts/modifying-the-schema.md) refused the deletion; the message enumerates the blockers by name. |
| `schema-violation` | 422 | Trial recompile failed; the write was rolled back (message carries the compiler's reason). |
| `would-create-cycle` | 409 | Move would place an object under itself or a descendant. |

---

# The base schema

_Universal Directory / Reference_

> Every attribute, facet, and class in UD's base schema — the 48 system definitions materialised at domain creation — with syntaxes, matching rules, compositions, and DN prefixes.

The **base schema** is the set of system definitions every directory is born with: 26 attributes, 11 facets, and 11 classes — 48 objects in all, flat under `/Configuration/Schema` and immutable forever.

All new definitions live in [namespaces](/universal-directory/concepts/modifying-the-schema.md); the unprefixed name space tabulated below is reserved. The base definitions are minted at **domain creation** by the first node, as its ordinary originating writes. There are no well-known GUIDs — a joining node receives everything by sync.

## Attributes

| Attribute | Syntax | Values | Matching | Aliases | legalMeta |
|---|---|---|---|---|---|
| `description` | string | multi | caseless | | |
| `displayName` | string | single | caseless | | |
| `givenName` | string | single | caseless | | |
| `surname` | string | single | caseless | `sn` | |
| `email` | string | multi | caseless | `mail` | |
| `sid` | string | single | exact | `objectSid` | |
| `member` | reference | multi | exact | | `expiry` |
| `expiry` | timestamp | single | exact | | |
| `syntax` | string | single | caseless | | |
| `singleValued` | boolean | single | exact | | |
| `matching` | string | single | caseless | | |
| `legalMeta` | reference | multi | exact | | |
| `aliases` | string | multi | caseless | | |
| `lifecycle` | string | single | caseless | | |
| `system` | boolean | single | exact | | |
| `attributes` | reference | multi | exact | | `required` |
| `required` | boolean | single | exact | | |
| `facets` | reference | multi | exact | | |
| `attachedFacets` | reference | multi | exact | | |
| `dnPrefix` | string | single | caseless | | |
| `targetClass` | reference | single | exact | | |
| `instantiable` | boolean | single | exact | | |
| `maxTimeApart` | integer | single | exact | | |
| `timeUntilShred` | integer | single | exact | | |
| `networkAddress` | endpoint | multi | caseless | | |
| `machineKey` | publicKey | multi | exact | | |

Notes: `description` is multi-valued (following the LDAP RFCs where they and Active Directory disagree). `attachedFacets` is engine-level — legal on every object regardless of surface, and the vehicle for [instance facet attachment](/universal-directory/concepts/the-schema-model.md). Aliases are recorded on definitions but not yet used for lookup. `expiry` and `required` exist to serve as meta keys (on `member` and `attributes` respectively). `instantiable` (unset = true) marks classes users may instantiate. `maxTimeApart` and `timeUntilShred` are the domain's lifecycle knobs, in seconds, held on the `/Configuration` object itself: `timeUntilShred` is the restore window (default 30 days — past it a soft-deleted record hardens to a skeleton), `maxTimeApart` the purge horizon and future partition contract (default 180 days — past it tombstones purge entirely). `networkAddress` holds a machine's network endpoints — multi-valued because a DNS name, an IPv4, and an IPv6 address are legitimately concurrent addresses of one machine.

### The `endpoint` syntax

`networkAddress` uses the **`endpoint`** value syntax: `host[:port]`, where the host is a DNS name, an IPv4 literal, or a **bracketed** IPv6 literal (`[2001:db8::1]:6000`), and an omitted port designates the well-known **GIP port, 5390** (GIP — the General Interlink Protocol — is UD's intra-domain machine-to-machine protocol; the syntax ships today, the protocol does not yet). The grammar is deliberately strict, because a loose syntax can never be tightened once values persist:

- no schemes (`quic://…`) and no whitespace anywhere;
- DNS names are ASCII (IDN arrives as punycode), dot-separated labels of letters, digits, and interior hyphens, ≤ 63 bytes per label and ≤ 253 total, with no trailing dot;
- anything whose last label is all digits must be a *valid* IPv4 literal — `999.1.1.1` is refused rather than silently treated as a DNS name — and leading zeros in octets (historical octal) are refused;
- IPv6 must be bracketed, zone ids (`%eth0`) are refused;
- an explicit port is 1–65535 with no leading zeros.

Values compare caselessly as opaque strings: `DC1.corp` and `dc1.CORP` are the same set member, but `dc1.corp` and `dc1.corp:5390` are two distinct values even though they designate the same place — set matching is literal; interpreting endpoints is the dialer's job.

### The `publicKey` syntax

`machineKey` uses the **`publicKey`** value syntax: `<algorithm>:<hex>`, where the only algorithm today is `x25519` and the payload is exactly 64 **lowercase** hex characters (32 bytes) — one canonical spelling per key, compared exactly. This is the machine's GIP identity key: [`udd new`](/universal-directory/getting-started/running-udd.md) mints the keypair at enrollment, keeps the private half in the data directory's `machine-key` file, and records the public half here, replicated like any other fact. The attribute is multi-valued so a future key *rotation* can hold old and new keys through an overlap window. The algorithm prefix is deliberate agility: a future signing key gets a new prefix, not a new syntax.

## Facets

| Facet | Attributes (`*` = required) |
|---|---|
| `described` | `description`, `displayName` |
| `person` | `givenName`, `surname`, `email` |
| `securityPrincipal` | `sid` * |
| `membership` | `member` |
| `schemaDefinition` | `aliases`, `lifecycle` *, `system` * |
| `attributeSchema` | `syntax` *, `singleValued` *, `matching` *, `legalMeta` |
| `facetSchema` | `attributes` |
| `classSchema` | `facets`, `dnPrefix`, `instantiable` |
| `extensionSchema` | `targetClass` *, `facets` * |
| `replicationConfig` | `maxTimeApart`, `timeUntilShred` |
| `machine` | `networkAddress`, `machineKey` |

Required-ness is enforced today only where compilation demands it (schema definitions must be born complete); enforcement for ordinary objects is pending.

`machine` is the surface for anything reachable on the network; both attributes are deliberately optional on it — a pre-staged machine that has not yet enrolled has neither an address nor a key. The `computer` class composes it, and [`udd new`](/universal-directory/getting-started/running-udd.md) creates one `computer` object for the machine it runs on. (The class is not called `machine` because definitions are siblings under `/Configuration/Schema`, where names are unique regardless of kind — the facet holds that name.)

## Classes

| Class | dnPrefix | Instantiable | Facets |
|---|---|---|---|
| `domainRoot` | — (never in a DN) | no | `described` |
| `organizationalUnit` | `OU` | yes | `described` |
| `user` | `CN` | yes | `described`, `person`, `securityPrincipal` |
| `group` | `CN` | yes | `described`, `securityPrincipal`, `membership` |
| `attributeDefinition` | `CN` | yes | `described`, `schemaDefinition`, `attributeSchema` |
| `facetDefinition` | `CN` | yes | `described`, `schemaDefinition`, `facetSchema` |
| `classDefinition` | `CN` | yes | `described`, `schemaDefinition`, `classSchema` |
| `schemaNamespace` | `OU` | yes | `described` |
| `classExtension` | `CN` | yes | `described`, `schemaDefinition`, `extensionSchema` |
| `configuration` | `CN` | no | `described`, `replicationConfig` |
| `computer` | `CN` | yes | `described`, `machine` |

The self-hosting anchor: the `classDefinition` object under `/Configuration/Schema` is an instance of itself; every other definition's kind resolves from there.

An instantiable class must carry a `dnPrefix` (compile-enforced); non-instantiable classes are engine-created only. `/Configuration` is the singleton `configuration` instance — its class composition is what makes the replication knobs writable there, and future config areas graft on via `classExtension`. Besides `/Configuration/Schema`, the base tree also contains **`/LostAndFound`**, a system OU born with the domain: the destination [replication repairs](/universal-directory/concepts/replication.md) derive orphans and broken cycles into (the domain root is a replication head from birth). It also contains **`/Domain Controllers`**, the default home for machine objects — a convention, not structure: nothing in UD locates domain controllers by path, and a machine object is free to live anywhere.

---

# Naming rules

_Universal Directory / Reference_

> The precise rules for names in UD: object-name validity, the pinned caseless folding, DN composition and RFC 4514 escaping, native paths, definition-name grammar, wire names, and the reserved descriptor x.

## Object names

An object's bare name must be:

- non-empty and at most 255 bytes,
- free of control characters,
- free of `/` (the native path separator),
- without leading or trailing whitespace.

Everything else — spaces, Unicode, `=`, `,` — is legal; DN composition escapes what needs escaping. These rules are deliberately strict: loosening a validity rule later is safe, tightening one is a data migration.

## Caseless comparison (pinned)

Sibling uniqueness, attribute-name lookup, path resolution, and `caseless`-matched value sets all compare under **Unicode canonical caseless matching** (Unicode §3.13): two strings are equal iff

```
NFD(casefold(NFD(a))) == NFD(casefold(NFD(b)))
```

So `Jack` = `jack`, `STRASSE` = `Straße` (ß case-folds to `ss`), and composed `é` = decomposed `e +  ́`. Stored spellings are always preserved; the folding governs comparison only. This function is pinned: changing it once data persists would shift uniqueness invariants underneath stored data.

## Distinguished names

DNs compose leaf-first from typed components: `CN=Jack,OU=Sales`. The component type is the object's class's `dnPrefix`; the value is the bare name escaped per **RFC 4514** — a backslash before any of `" + , ; < > \` anywhere, before a leading space or `#`, and before a trailing space. The root object contributes nothing; its DN is the empty string.

## Native paths

The untyped address form: `/` followed by bare names joined with `/`, root-first — `/Sales/Jack`, resolving case-insensitively. The root's path is `/`. Because `/` is banned in names, paths are unambiguous.

## Definition names and wire names

Schema definition names (attributes, facets, classes, extensions) follow a stricter grammar than object names: **a letter followed by letters or digits** — camelCase by convention, and **no hyphens**, because the hyphen is the namespace separator. **Namespace names** additionally allow **dots** between segments (each segment a letter followed by letters or digits): `Acme` and reverse-DNS `com.acme.tools` are both legal, and reverse-DNS is the convention for anything redistributable — it makes accidental collisions between vendors nearly impossible. A namespaced definition's **wire name** is `Namespace-name` (`Acme-badgeColor`, `com.acme.tools-badgeColor`) — the single hyphen is unambiguous because neither side may contain one. Base definitions are unprefixed, and that unprefixed space is [reserved forever](/universal-directory/concepts/modifying-the-schema.md).

The namespace names `gip` and `ud` (caselessly) are **permanently reserved**: they are GIP's service namespaces (`gip/relay`, `ud/drs`), and service names share the schema's namespace universe.

`dnPrefix` values follow RFC 4512 keystring grammar (a letter, then letters, digits, or hyphens).

## CNF display names

When replication merges two live siblings whose names fold equal, the earliest placement claim keeps the name and the later one *displays* as `<stored name> CNF:<guid>` (laddering ` (2)`, ` (3)`… past pathological collisions). This is a derived view, not a rename: the stored name is untouched, the CNF form resolves in paths, and an ordinary rename dissolves it.

## The reserved descriptor `x`

The name `x` (caselessly) is **permanently reserved** and can never be defined as an attribute, facet, class, extension, or namespace name. UD reserves it as its untyped-lookup descriptor so that no schema element can ever collide with that use.
