Classes and instances
Quayside has a deliberate three-layer identity model. Each layer answers a different question.
Class
A class is the durable, governable “kind of agent” — eng/code-reviewer, support/tier1-bot, finance/expense-classifier. It is what your registry contains, what policies attach to, and what audit aggregates against.
A class has:
| Field | Notes |
|---|---|
slug | URL-safe identifier, must be unique. Lower-case, hyphens, slashes. eng/code-reviewer is fine. |
name | Human-readable. Shown in the dashboard. |
purpose | What it does, in a sentence. Used at registration for human review and surfaced in audit. |
owner_principal_id | A real human, not a team alias. Accountability lives here. |
lifecycle_status | One of draft / active / deprecated / sunset / external. |
supersedes | Optional reference to another class id this one replaces. |
There are tens to low-hundreds of classes in a real organisation — not thousands.
Lifecycle
draft ──approve──▶ active ─┬─► deprecated ──► sunset (terminal) └─► sunset (emergency skip)external ──promote──▶ active (shadow → real)external ──reject──▶ sunset (shadow → terminal)- draft — registered, not yet active. The proxy refuses calls from a draft class. Use this if your organisation wants a two-step approval flow.
- active — the normal state. The proxy accepts calls.
- deprecated — the proxy still accepts calls, but the dashboard surfaces deprecated classes for migration. Use this when you want to phase a class out without breaking existing callers.
- sunset — the class is rejected. Audit data remains.
- external — shadow-discovery state. A class id that the proxy saw via the shadow log but was never explicitly registered. Promote or reject it from the dashboard.
The transition matrix is enforced by the service. sunset is terminal — you cannot un-sunset a class.
Instance
An instance is the runtime identity of a specific (class, principal) pair. Same class slug + same principal id always resolves to the same instance. Restart, crash, machine swap — same instance id.
The proxy claims instances automatically on first call. No CRUD surface — they exist as a side effect of using the system.
The schema is small:
| Field | Notes |
|---|---|
id | UUID |
class_id | FK to the class |
principal_id | Who is calling on behalf of the class |
first_seen_at | When the (class, principal) pair was first observed |
last_seen_at | Updated on every call |
Instances are where you go to answer “who has actually used this class?”
Session
A session corresponds to one audit run — one trip through the proxy. The current schema doesn’t store sessions as a top-level table; the runs table is the audit-side projection.
A run carries:
- the
instance_idof who made the call - the
started_at/finished_attimestamps - the
final_effect(Allow,Flag,Block, …) - every step (one per detector inspection) attached via
run_id - the token usage row attached via
run_id
How they compose
Policy ────► attaches to Class │ ▼ Instance (auto-claimed for each user × class) │ ▼ Run / steps / token usage (one set per call)Class is what humans care about. Instance is what the proxy tracks. Run is what audit reports against.
Why the model is shaped this way
You want policy attached to “the kind of bot” — not to each user. You want fleet questions answered at the class level. You want per-user usage answered at the instance level. You want per-call records answered at the run level. Three different aggregation grains, three different layers.