Architecture

Technical detail for the operator or engineer implementing a space inside Hue. For the audience-facing overview, read What is Hue.

Tenancy

Edges — the knowledge-graph core

The edges table is where every relationship lands. One row is (subjectType, subjectId) --[predicate]--> (objectType, objectId) with optional properties, displayText, embedding, sourceSkill, createdBy, createdAt. Schema at src/core/schema/edges.ts.

Indexes

Three composite btree indexes scoped by workspace:

Plus a 6-tuple edges_unique_tuple unique index for idempotent re-emission, and an edges_embedding_hnsw_idx HNSW cosine index for the semantic layer.

Walks

graph.walk runs typed BFS in application code, issuing one SELECT per hop against the composite index. Up to 6 hops, bounded by maxNodes. Complexity is O(edges-touched × log n) on an HNSW-covered edges table.

Verified on synthetic fixtures to low millions of edges per tenant. Sub-second two-hop walks at 100M edges are projected from HNSW and btree recall / latency benchmarks published by the pgvector maintainers (single-node Postgres, SSD-backed, standard cloud instance sizes). Real tenants past ~10M are the first benchmark opportunity; this page will carry measured numbers once that's available.

Partitioning

When a single tenant approaches ~100M edges, declarative Postgres partitioning on workspace_id splits edges into per-partition tables. Composite indexes and the HNSW index attach per-partition. Zero application-side change — the Drizzle layer keeps the same from(edges) calls; the planner chooses the right partition based on the workspace_id predicate.

Semantic layer

Alongside exact edges, pgvector provides cosine-similarity search via:

Only Hue-native displayText is embedded. Source-service payloads (Gmail message bodies, CRM record fields, etc.) are never vectorised — the graph is a federated pointer map, not a warehouse.

Surfaces

Every defineSkill() auto-exposes on three surfaces simultaneously:

Same auth, same rate limits, same audit log regardless of surface.

Skill execution pipeline (executeSkill)

In order, for every call:

  1. Load the skill from the registry; 404 if unknown.
  2. Permission check via checkPermission(userId, workspaceId, service, skill, action) unless classification is public.
  3. Input validation against the skill's Zod schema.
  4. isServiceEnabled(plugin, spaceId) — SERVICE_DISABLED if the space has toggled this service off.
  5. Scope substitution — for each scope.fields[], replace the input field with the space's pinned value unless the caller holds scope.set access.
  6. Idempotency gate — if the skill is idempotent: true and the caller presented Idempotency-Key, look up the hash; return cached response if hit.
  7. PII gate — if the space has PII redaction on, scrub input strings.
  8. Input filters chain.
  9. Handler — wall-clock budget timer if declared; ctx.spend tracks cost.
  10. Output filters chain.
  11. Fires declared hooks.
  12. Output validation against the Zod output schema.
  13. Billing cost computation (billing.costUsd).
  14. Audit log row (hash-chained, redacted per classification).
  15. Lifecycle end hook.
  16. Report-card push into context store if the skill declares one.
  17. PII redaction on output.
  18. Return.

The three layers

Architecture is best understood as a three-layer mind:

CORTEX   = context lake  → "what matters in this space"  (soft recommendations)
GRAPH    = edges         → walkable ref-to-ref relationships  (user-authored only)
SERVICES = connectors    → ground truth, re-fetched on demand

An LLM session on a Space reads the cortex first (spaces.listPinned) to orient itself, walks the graph when it needs to trace a connection (graph.walk / graph.neighbors / graph.searchSemantic), and hits a service (<ref>.fetchSkill) when it needs live data. Meaning is derived on demand, never warehoused.

Primitives and their storage

PrimitiveWhereCascadeNotes
Accessworkspace_access + permission_overridesNoExplicit per-(space, human).
Contextcontext_store(namespace, key, workspaceId, userId)Yes (closest-wins)SASS-like.
Connectorsconnector_auth(provider, workspaceId)YesWalks up parent chain.
Context lake (pins)context_store(namespace='pinned', key='references', workspaceId)Yes (concat-array, dedupe-by-newest)SOFT default. Cortex layer. List skills read pins as soft filters.
Scopecontext_store(namespace=<plugin>, key='_scope', workspaceId)YesHARD lock. Executor substitutes on skill input; throws OUT_OF_SCOPE.
Service enablementcontext_store(namespace=services, key=<plugin>, workspaceId)YesDefault = enabled.
Auditaudit_log (hash-chained)NoAppend-only.
EdgesedgesNo (per-space)Per-tenant schema scales partitions per-tenant. User-authored ref-to-ref only; pins do NOT emit edges.
Embeddingsedges.embeddingNoHNSW cosine index.

Lake vs scope — the env-var analogy. Both persist to context_store; their semantics are opposite:

Writing a new service

Deploy with ./scripts/safe-build.sh. Four drift guards (manifests:check, describe:check, schema:diff, plugin:lint) block on any inconsistency.

See also