| title | Architecture Overview | ||||||
|---|---|---|---|---|---|---|---|
| type | design | ||||||
| tags |
|
||||||
| created | 2026-03-26 | ||||||
| updated | 2026-08-09 |
This document provides a comprehensive overview of the Index Network architecture for new contributors, stakeholders, and anyone seeking to understand how the system is structured. It covers the monorepo layout, protocol layering, agent system, data flow, and supporting infrastructure.
For domain-specific deep dives, see docs/design/protocol-deep-dive.md, the protocol README at packages/protocol/src/README.md, and the current lifecycle reference at docs/design/opportunity-status-lifecycle.md. Research and historical analysis live under docs/research/.
The repository is organized as a Bun-managed monorepo with user-facing apps, deployable services, and reusable packages.
index/
apps/
web/ Vite + React Router v7 SPA (React 19, Tailwind CSS 4)
mac/ Native macOS client subtree (Swift WKWebView prototype)
services/
api/ Backend API and Agent Engine (Bun, TypeScript)
packages/
protocol/ @indexnetwork/protocol NPM package (agent graphs, interfaces, tools)
cli/ CLI client (@indexnetwork/cli, Bun, TypeScript)
claude-plugin/ Claude/Codex plugin distribution, subtree-synced publicly
hermes-plugin/ Hermes-native plugin distribution with generated skills, subtree-synced publicly
API service is a native Bun HTTP server (Bun.serve) running on port 3001. It hosts the API, LangGraph-based agent system, database layer, job queues, and event infrastructure.
Web app is a single-page application built with Vite and React Router v7. In development, Vite proxies /api/* requests to the API service. In production, apps/web/server.ts serves the Vite output and falls back to index.html only for document navigations. HTML is no-store, generated /assets/* files are immutable, and missing assets return a non-HTML 404 so stale lazy imports can be detected and recovered safely. React Router lazy imports make one bounded, URL-preserving reload attempt per stale route load before the application error boundary presents an explicit refresh action.
Mac app is a subtree-synced native macOS prototype under apps/mac/ (Swift WKWebView shell wrapping a self-contained React/HTML bundle). is a standalone command-line client (@indexnetwork/cli) that wraps the Tool HTTP API. It provides authentication, command parsing, formatted terminal output, and --json mode for machine-readable output. Published to npm with platform-specific native binaries.
The Bun workspaces share the same repository and are installed together via bun install at the root. Development uses git worktrees (.worktrees/) to isolate feature and fix branches from the stable dev branch.
The protocol backend enforces strict layering to maintain separation of concerns and testability. Dependencies always point inward, from the HTTP boundary toward infrastructure.
+------------------------------------------------------------------+
| |
| Controllers |
| HTTP handlers, input validation, response formatting |
| Imports: services, guards, decorators |
| |
+------------------------------------------------------------------+
|
| delegates to
v
+------------------------------------------------------------------+
| |
| Services |
| Business logic, DB transactions, event emission |
| Imports: adapters, lib/protocol (graphs, agents) |
| |
+------------------------------------------------------------------+
|
| uses
v
+------------------------------------------------------------------+
| |
| Adapters |
| Own types that align with protocol interfaces |
| (database, embedder, cache, queue, scraper, storage) |
| Named by concept, not technology |
| |
+------------------------------------------------------------------+
|
| talks to
v
+------------------------------------------------------------------+
| |
| Infrastructure |
| PostgreSQL + pgvector, Redis (BullMQ), OpenRouter (LLM), |
| S3 (storage), external APIs |
| |
+------------------------------------------------------------------+
The protocol layer (packages/protocol/src/) sits alongside services. It contains LangGraph graphs, AI agents, tools, state definitions, and interfaces. It is fully self-contained — zero API/web/concrete-adapter imports. All infrastructure dependencies are received via constructor injection through interfaces defined in packages/protocol/src/shared/interfaces/. The API composition root (services/api/src/controllers/mcp.controller.ts) assembles ProtocolDeps inline and injects ChatGraphFactory into ChatSessionService at startup.
| Layer | Responsibility | Can Import |
|---|---|---|
| Controllers | HTTP handling, input validation via Zod, response formatting | Services, guards, decorators |
| Services | Business logic, DB transactions, event emission, typed results | Adapters, lib/protocol |
| Adapters | Define own types aligned with protocol interfaces, wrap infrastructure | Infrastructure libraries (not lib/protocol/) |
| Protocol | Graphs, agents, tools, state machines | Nothing external (all deps injected) |
| Infrastructure | PostgreSQL, Redis, OpenRouter, S3 | N/A (external systems) |
Layering is enforced through strict import rules. Violations cause tight coupling and make testing difficult.
Controllers
- CAN import: services, decorators (
@Controller,@Get,@Post), guards (AuthGuard) - CANNOT import: adapters, database, schema, Drizzle operators
Services
- CAN import: adapters from
src/adapters/, protocol graphs and agents from@indexnetwork/protocol - CANNOT import: other services (use events, queues, or shared lib for cross-service orchestration)
Adapters
- CAN import: infrastructure libraries (must not import from
@indexnetwork/protocolinterfaces — define own aligned types) - CANNOT import: services, controllers
Protocol layer (graphs, agents, tools)
- CAN import: only its own submodules and types
- CANNOT import: adapters or infrastructure directly (everything is injected)
Graph factories do not depend on the full Database interface. Instead, each factory declares a narrowed type using TypeScript's Pick<> utility. This documents exactly which database methods a graph needs and prevents accidental coupling.
// Full interface has 80+ methods
export interface Database {
getUser(id: string): Promise<UserRecord | null>;
getIntent(id: string): Promise<IntentRecord | null>;
assignIntentToIndex(intentId: string, indexId: string, score: number): Promise<void>;
// ... many more
}
// Each graph picks only what it needs
export type IntentNetworkGraphDatabase = Pick<
Database,
| 'getIntentForIndexing'
| 'getNetworkMemberContext'
| 'isIntentAssignedToIndex'
| 'assignIntentToNetwork'
| 'unassignIntentFromIndex'
| 'getIntent'
| 'isNetworkMember'
| 'isIndexOwner'
| 'getNetworkIdsForIntent'
| 'getNetworkIntentsForMember'
| 'getIntentsInIndexForMember'
>;
// Factory constructor accepts the narrow type
export class IntentNetworkGraphFactory {
constructor(private database: IntentNetworkGraphDatabase) {}
}This pattern is applied to all graph factories: EnrichmentGraphDatabase, OpportunityGraphDatabase, IntentGraphDatabase, NetworkGraphDatabase, IntentNetworkGraphDatabase, NetworkMembershipGraphDatabase, HydeGraphDatabase, and RadarGraphDatabase.
Adapters are named by concept, not by implementation technology.
| Correct | Incorrect |
|---|---|
database.adapter.ts |
drizzle.adapter.ts |
cache.adapter.ts |
redis.adapter.ts |
queue.adapter.ts |
bullmq.adapter.ts |
storage.adapter.ts |
s3.adapter.ts |
This allows swapping infrastructure without renaming files or updating imports across the codebase.
The agent system is built on LangGraph (from the LangChain ecosystem) and follows a consistent architecture: graphs orchestrate workflows, agents perform LLM reasoning, tools expose capabilities to the chat agent, and state carries data through the pipeline.
packages/protocol/src/
chat/, intent/, enrichment/, premise/, opportunity/, negotiation/
Feature-owned graphs, agents, state, and tools
network/ Network graph, with indexer/ and membership/ subgraphs
agent/, contact/, context/, integration/, maintenance/, mcp/, questioner/
Supporting protocol capabilities
shared/ Cross-cutting agent runtime, adapter interfaces, schemas,
HyDE, assignment, observability, UI, and utilities
Graphs are LangGraph state machines. Each graph is created by a factory class that accepts dependencies via constructor injection.
| Graph (factory) | Purpose |
|---|---|
| Chat | ReAct agent loop with tool calling |
| Intent | Extract, verify, reconcile, and persist intents |
| Enrichment | Enrich users and decompose identity into premises, with optional scraping |
| Opportunity | HyDE-based discovery: search, evaluate, rank, persist |
| HyDE | Generate hypothetical document embeddings (cache-aware) |
| Network | Manage network (network) CRUD |
| NetworkMembership | Manage network member join/leave |
| IntentNetwork | Evaluate and assign/unassign intents to indexes |
| Radar | Build the radar view: flat presenter-card list |
| Maintenance | Periodic maintenance tasks |
| Negotiation | Multi-turn negotiation flows |
Graph invariants: Every graph must have at least one conditional edge. All graphs use Annotation.Root with reducers for state management. Nodes are async functions that accept state and return partial state updates. Nodes catch errors internally rather than throwing.
Agents are pure LLM reasoning units. They accept structured input (Zod schemas), call the LLM via createModel() from shared/agent/model.config.ts, and return structured output. Agents have no direct database access and no side effects. Services handle persistence after agent execution.
| Agent | Purpose |
|---|---|
| ChatAgent | Orchestrates tool calls in the ReAct loop |
| Intent Inferrer | Extracts intents from uploaded content |
| Intent Reconciler | Decides create/update/expire actions for intents |
| Intent Verifier | Validates felicity conditions on intents |
| Intent Indexer | Scores intent-to-network fit (relevancy 0.0-1.0) |
| Opportunity Evaluator | Scores and synthesizes opportunity matches |
| Enrichment Generator | Generates identity drafts from identity signals (onboarding draft tools) |
| HyDE Generator | Creates hypothetical document embeddings |
Tools are the capabilities exposed to the chat agent. They bridge the agent loop and the subgraph layer. When the chat agent decides to call a tool, the tool function invokes the appropriate subgraph.
| Tool File | Capabilities |
|---|---|
enrichment.tools.ts |
read/create/update user profiles |
intent.tools.ts |
CRUD intents, manage intent-network assignments |
network.tools.ts |
CRUD networks (networks), manage memberships |
contact.tools.ts |
import, add, remove, and list contacts |
opportunity.tools.ts |
Discover and send opportunities |
agent.tools.ts |
register, list, update, delete agents and manage agent permissions |
integration.tools.ts |
Connect and manage third-party integrations |
negotiation.tools.ts |
Respond to negotiation turns |
chat.tools.ts |
Chat session and conversation tools |
utility.tools.ts |
URL scraping, action confirmation/cancellation |
The protocol includes an agent registry that sits beside the chat tool stack and the negotiation system. It gives every actor in the system — system agents like Index Chat Orchestrator and Index Negotiator, as well as user-owned personal agents connected from OpenClaw, Claude Code, Codex, or any MCP-capable runtime — a first-class database identity that can be authenticated, authorized, and dispatched against.
agentsstores personal and system agent identities (type: 'system' | 'personal',ownerId,status).agent_transportsstores delivery channels. The only channel ismcp— the agent authenticates with an API key bound to its identity, connects to the MCP server for tool work, and pollsPOST /api/agents/:id/negotiations/pickupfor negotiation turns.agent_permissionsstores the actions an agent may perform for a user (e.g.manage:intents,manage:negotiations), optionally scoped to a network or node.
System agents are seeded with fixed UUIDs and granted their default permissions during onboarding. Personal agents are user-owned records exposed through the /api/agents controller family (see docs/specs/api-reference.md).
MCP requests authenticate via an x-api-key header. The resolver reads the Better Auth metadata.agentId stored on the token and hands back { userId, agentId } to the MCP server factory. Tool handlers receive both on the ResolvedToolContext, so every tool call is attributable to a concrete agent identity — not just a user. MCP callers without a resolved agentId are blocked from all tools except register_agent and read_docs by the principal-aware capability policy inside createMcpServer. (Contact/Gmail tools, scrape_url, and the deprecated profile/profile-run aliases are not exposed on the MCP surface at all — they remain available via the direct HTTP Tool API and chat.)
Every tool and negotiation endpoint checks the caller's agent_permissions for the relevant action (e.g. the negotiation pickup/respond endpoints and the respond_to_negotiation MCP tool require manage:negotiations). MCP auth resolves the (userId, agentId) pair from the API key, so every permission check is attributable to a concrete agent identity — not just a user.
agent_permissions.scope accepts 'global' | 'node' | 'network'. A network-scoped permission row — scope='network', scopeId=<networkId> — restricts the agent to a single network. Two enforcement layers:
- HTTP:
services/api/src/guards/agent-scope.guard.tsexposesresolveAgentNetworkScope(req),assertAgentNetworkScope(req, networkId), andwithAgentScope(req, user). Network/intent/opportunity controllers assert on writes that take a path-param networkId, and filter list endpoints viawithAgentScope. Mismatches throwScopeViolationError, mapped to HTTP 403 inmain.ts. - MCP: the auth resolver also returns
networkScopeId.applyNetworkScopeToContext(inpackages/protocol/src/mcp/mcp.server.ts) promotes that binding intoResolvedToolContext.scopeType/scopeId; tools derive concrete allowed network IDs from the scope envelope plus memberships, and the per-requestsystemDbis constructed from the same derived set, so every downstream tool call is bounded at both the prompt-visible scope and the DB-level scope check. - Chat tools (shared): the same
[scopedNetwork, personalIndex]clamp applies on the web-chat path viascopeType/scopeId(withnetworkIdaccepted only as a REST/session edge alias) — so an MCP-scoped agent and a web-scoped chat see the same data perimeter.createChatToolsconstructssystemDbfrom network IDs derived from the scope envelope plus memberships, keeping the prompt-advertised reach and the DB-level clamp consistent. - DB (opportunity reads):
OpportunityDatabaseAdapter.getOpportunitiesForUserrequires the requesting user's own actor entry to be anchored on the bound network —EXISTS actor WHERE userId=$1 AND networkId=$2, not two independentactors @>checks.update_opportunitymirrors the rule.actors[].networkIdis the source of truth for scope; theopportunity.context.networkIddenormalization is no longer consulted by security-relevant filters.
The primary use case is bulk experiment-network onboarding: networkInvitationService.invite({ networkId, email }) provisions user + network-scoped agent + API key + invitation email. Possession of the email account is the user's verification — there is no separate users.experimentNetworkId column anymore.
Negotiation turns that cannot be resolved synchronously by an in-process system agent are parked for polling: the graph writes a tasks row in waiting_for_agent with the full turn context in metadata and suspends.
- The user's personal agent polls
POST /api/agents/:id/negotiations/pickupwith its API key. The backend atomically CAS's the oldest pending task for the caller's user fromwaiting_for_agenttoclaimed, enqueues a 6-hour claim timeout, and returns the turn context. - The agent deliberates and submits its decision via
POST /api/agents/:id/negotiations/:negotiationId/respond. The backend persists the turn and either finalizes the negotiation (onaccept,reject, or turn cap) or returns the task towaiting_for_agentfor the counterparty. - If no agent claims a parked turn within 24 hours, the in-process system
Index Negotiatortakes over.
Personal agents poll POST /api/agents/:id/negotiations/pickup with their API key. On a successful pickup the agent reads the turn context, deliberates, and submits the response via POST /api/agents/:id/negotiations/:negotiationId/respond. See docs/domain/negotiation.md for the full turn protocol.
Chat Tools ----invoke----> SubGraphs ----call----> Agents
| | |
| | | (LLM reasoning)
| | v
| | Structured output
| | |
| v |
| State machine returned to
| (nodes + edges) graph node
| |
v v
Tool result Persisted to DB
returned to (via injected database)
ChatAgent
A typical user request flows through the following layers.
User (Browser/Client)
|
| HTTP request (POST /api/chat/web/stream)
v
Bun.serve (main.ts, port 3001)
|
| Route matching via RouteRegistry
v
Guard (AuthGuard)
|
| Validates session, resolves user
v
Controller (ChatController)
|
| Input validation (Zod), delegates to service
v
Service (ChatService / Graph invocation)
|
| Business logic, invokes graph factory
v
Graph (ChatGraphFactory.createGraph())
|
| State machine execution: nodes, conditional edges
v
Agent (ChatAgent / specialized agents)
|
| LLM call via OpenRouter, structured output
v
Database (via injected adapter)
|
| Drizzle ORM, PostgreSQL + pgvector
v
Response (JSON / SSE stream back to client)
The chat system is the primary entry point for user interaction. When a user sends a message:
-
HTTP layer: The request hits
ChatController, which validates input and delegates to the chat service. -
Graph initialization: The chat graph loads session context (conversation history, user profile, network memberships) and truncates to fit the context window.
-
ReAct loop: The
ChatAgententers a loop (up to 12 iterations). Each iteration, the LLM sees the full conversation and decides to either call tools or produce a final response. -
Tool execution: When the agent calls tools (e.g.,
create_intent), each tool invokes the appropriate subgraph. For example,create_intentinvokes the Intent Graph, which runs the inferrer, verifier, and reconciler agents in sequence. -
Subgraph execution: The subgraph runs its own state machine. Nodes perform database operations through the injected adapter. Agents make LLM calls for reasoning.
-
Result propagation: Tool results flow back to the chat agent as
ToolMessageobjects. The agent incorporates these results and either calls more tools or produces a final response. -
Streaming: The response is streamed back to the client via SSE (Server-Sent Events).
When a user says "I'm looking for a React co-founder":
- The chat agent calls
create_intentwith the extracted content - The Intent Graph runs:
- Prep node: Loads user context
- Inference node:
IntentInferrerextracts structured intent from natural language - Verification node:
IntentVerifierchecks felicity conditions (semantic entropy, referential anchors, sincerity) - Reconciliation node:
IntentReconcilerdecides whether to create, update, or expire existing intents - Execution node: Persists the intent to the database with embedding
IntentEvents.onCreatedfires, which enqueues an opportunity discovery job- The opportunity queue picks up the job asynchronously
PATCH /api/intents/:id/status accepts only ACTIVE and PAUSED. The transition is owner-scoped, re-checks a bound agent's network scope against intent_networks, and rejects archived or terminal (FULFILLED/EXPIRED) intents. Null legacy status is normalized to ACTIVE.
Pause is an admission gate, not cleanup. It preserves existing opportunities/Radar cards, pending questions, conversations, intent-network assignments, and HyDE documents. Lifecycle checks prevent a paused intent from admitting not-yet-started intent-driven discovery, appearing as a candidate match, or starting new pool mining, question generation, and answer-triggered Tier-1 runs. Work that passed its admission check before the pause may finish. A pending question can still be answered and its deterministic Tier-0 re-ranking can still affect the existing pool.
Resume atomically restores ACTIVE and invokes IntentEvents.onResumed. The HTTP response awaits enqueue acknowledgement for a from-intent discovery job whose ID includes the stable lifecycle version, so retries deduplicate. If enqueue fails after a real PAUSED → ACTIVE transition, a narrow owner/scope/version compare-and-set compensates back to PAUSED; concurrent lifecycle writes are not overwritten, and an idempotent ACTIVE request is not mutated. The endpoint returns retryable 503 enqueue_failed with the authoritative resulting status instead of claiming success. An acknowledged run proceeds through the ordinary discovery-completion pool mining and question flow.
The event system provides async decoupling between services. Events are lightweight hooks defined in src/events/ and wired up in main.ts.
Defined in src/events/intent.event.ts:
export const IntentEvents = {
onCreated: (_intentId: string, _userId: string): void => {},
onPaused: (_intentId: string, _userId: string, _lifecycleVersionMs: number): void => {},
onResumed: async (_intentId: string, _userId: string, _lifecycleVersionMs: number): Promise<void> => {},
onArchived: (_intentId: string, _userId: string): void => {},
};These are assigned concrete handlers in main.ts. onCreated enqueues discovery and triggers opportunity maintenance; onPaused records the transition without cleanup; onResumed awaits enqueue acknowledgement for the deduplicated resume discovery job; and onArchived triggers maintenance after archive cleanup. The awaited resume handler makes the status response acknowledge queue admission rather than merely starting a fire-and-forget enqueue; service-level compare-and-set compensation restores PAUSED when a changed resume cannot be admitted:
IntentEvents.onResumed = async (intentId, userId, lifecycleVersionMs) => {
await fromIntentQueue.addJob(
{ intentId, userId, trigger: 'intent_resume' },
{ priority: 10, jobId: intentResumeDiscoveryJobId(userId, intentId, lifecycleVersionMs) },
);
};Defined in src/events/network_membership.event.ts:
export const NetworkMembershipEvents = {
onMemberAdded: (_userId: string, _networkId: string): void => {},
};When a user joins an index, this event triggers an enrichment job (ensure_profile_hyde) so the new member becomes discoverable via vector search within that index.
- Services emit events after DB transactions, ensuring data consistency before side effects
- Events decouple services: the intent service does not need to know about opportunity discovery
- Queue-based handlers: event handlers enqueue jobs rather than executing work inline, keeping the request path fast
- Events and queues are the only mechanism for cross-service communication (services must not import other services)
BullMQ (backed by Redis) handles all asynchronous processing. Queue definitions live in src/queues/, and workers are started in main.ts.
| Queue | Purpose |
|---|---|
intent.queue |
Intent indexing and generation jobs |
opportunity/from-intent |
BullMQ queue: intent-triggered opportunity discovery |
opportunity/from-introducer |
BullMQ queue: introducer-triggered opportunity discovery |
opportunity/expiration |
node-cron task (not a BullMQ queue — does not appear in Bull-Board): scans and expires stale opportunities on a schedule |
negotiations/run-existing |
BullMQ queue: enqueue bilateral negotiation for an existing opportunity (e.g. after introducer approval) |
negotiations/timeout |
BullMQ queue: AI fallback when personal agent lacks heartbeat |
negotiations/claim-timeout |
BullMQ queue: expire stale claims stuck in claimed state |
enrichment.queue |
User enrichment (premise decomposition) and HyDE document creation |
hyde.queue |
HyDE document generation and cron-based refresh |
email.queue |
Email delivery via Resend |
notification.queue |
Notification delivery |
integration-sync-queue |
Periodic Google Calendar sync for event networks |
frame-drift-monitoring |
Disabled-by-default daily BullMQ scheduler for atomically claimed, immutable capture-time centroid observations and a non-causal intent-assignment-pair normalized opportunity-yield proxy; intentionally omitted from Bull Board |
- Retries: 3 attempts with exponential backoff (1-second base delay)
- Cleanup: Completed jobs removed after 24 hours, failed jobs after 7 days
- Concurrency: Default is 1 (sequential processing) to avoid race conditions
- Naming: Snake_case job names (e.g.,
generate_hyde,discover_opportunities) - Deduplication: Jobs use deterministic IDs where appropriate (e.g., time-bucketed rediscovery jobs) to prevent duplicate processing
Queues orchestrate by calling services, graphs, or adapters. They contain no business logic themselves. A queue handler might:
- Load context from the database adapter
- Invoke a graph factory to run a pipeline
- Persist results via the adapter
- Emit events if further processing is needed
Bull Board UI is served at http://localhost:3001/dev/queues/ when the protocol server is running. It provides job status visibility, retry controls, and queue metrics. Internal measurement schedulers such as frame-drift monitoring are not registered there.
Daily frame-drift measurement is implemented as queue orchestration → service calculation → adapter-owned REPEATABLE READ observation. A unique run header claims the whole bucket before source reads, and metric rows reference that header, preventing duplicate captures from appending newly eligible rows. Privacy thresholding applies both to user-balanced centroids and to each side of a yield pair; historical qualifying aggregates are not recomputed after later user deletion. The pipeline is measurement-only and has no API/UI or realignment side effects. See Frame-Drift Monitoring.
- ORM: Drizzle ORM with full TypeScript type inference from schema
- Database: PostgreSQL with the pgvector extension for vector similarity search
- Embeddings: 2000-dimensional vectors from
text-embedding-3-largevia OpenRouter - Indexes: HNSW indexes for fast approximate nearest-neighbor search
The canonical schema lives in services/api/src/schemas/database.schema.ts. All table definitions, relations, and types are defined here. Drizzle generates TypeScript types from the schema, eliminating manual type maintenance.
| Table | Purpose |
|---|---|
users |
User accounts (Better Auth integration); also the home of identity (name, bio via intro, location) |
intents |
User intents with embeddings, confidence scores, semantic governance fields |
networks |
Communities/collections (indexes); personal networks have isPersonal=true |
network_members |
Membership with permissions, custom prompts, auto-assignment settings |
intent_networks |
Many-to-many junction with optional relevancyScore (0.0-1.0) |
personal_networks |
Maps each user to their personal network (one row per user) |
opportunities |
Match records with detection, actors, interpretation, context, status |
hyde_documents |
Stored HyDE documents for retrieval |
conversations |
Conversation containers (A2A context) |
messages |
A2A-compatible messages with parts (JSONB), role, senderId |
tasks |
A2A task lifecycle (submitted, working, completed, failed) |
artifacts |
Structured outputs from tasks (opportunity cards, etc.) |
Polymorphic source tracking: Intents track their origin via sourceType (file, integration, link, discovery_form, enrichment) and sourceId, enabling filtering and bulk re-processing by source.
Confidence and inference tracking: Every intent carries a confidence score (0-1) and inferenceType (explicit or implicit), plus semantic governance fields from the verifier (semantic entropy, referential anchor, felicity scores).
Soft deletes: Records use deletedAt timestamps rather than hard deletes, preserving audit trails and enabling recovery.
Vector similarity search: Intents and premises have vector embeddings. Queries use pgvector's cosine similarity with HNSW indexes for sub-millisecond approximate nearest-neighbor lookups. This powers opportunity discovery, finding similar intents and premises across network members.
Drizzle generates migrations from schema diffs. Migrations are renamed to descriptive names following the pattern {NNNN}_{action}_{target}.sql (e.g., 0005_add_opportunities_table.sql). The _journal.json file tracks applied migrations and must stay in sync with .sql filenames.
+========================+
| Controllers | HTTP boundary
| (Bun.serve + decorators)| Input validation, routing
+========================+
|
v
+========================+
| Services | Business logic
| (pure TypeScript) | DB transactions, events
+========================+
|
+----+----+
| |
v v
+==========+ +========================+
| Adapters | | Protocol Layer |
| (infra | | (graphs, agents, tools) |
| wrappers)| | Deps injected via |
| | | constructor |
+==========+ +========================+
| |
v v
+========================+
| Infrastructure |
| PostgreSQL, Redis, |
| OpenRouter, S3 |
+========================+
Browser --HTTP--> Bun.serve --route--> Guard --auth--> Controller
| |
| delegates to
| |
| v
| Service
| |
| invokes graph
| |
| v
| Graph (state machine)
| |
| calls agents
| |
| v
| Agent (LLM call)
| |
| structured output
| |
| v
| Database (adapter)
| |
<-------------------SSE stream / JSON------------------+
+------------------+
| User message |
+--------+---------+
|
v
+------------------+
| Load context |
| (history, profile|
| memberships) |
+--------+---------+
|
v
+------------------------+
+---->| LLM Iteration |
| | (see full conversation|
| | + tool results) |
| +-----------+------------+
| |
| +-------+-------+
| | |
| Tool calls Final response
| | |
| v v
| +-------------+ +------------------+
| | Execute | | Stream to user |
| | tools in | | via SSE |
| | parallel | +------------------+
| +------+------+
| |
| Tool results
| (ToolMessage)
| |
+---------+
(up to 12 iterations)
Service
|
| 1. Persist to DB
| 2. Emit event
|
v
IntentEvents.onCreated(intentId, userId)
|
| Enqueues job
v
fromIntentQueue.addJob({intentId, userId})
|
| Worker picks up job
v
OpportunityGraphFactory.createGraph().invoke(...)
|
| HyDE generation -> vector search -> evaluation -> persist
v
New opportunities (status: latent)
Index presents one owner-scoped Personal Agent. Its name, avatar, memory, policy, and negotiation history never belong to an executor. The server stores a separate runtime binding that selects Index or a Hermes installation; Hermes is a security principal/execution binding, not a second persona. The Index macOS app is an optional owner-control client, not a dependency for standalone Hermes.
Authentication uses the two pre-existing credential concepts:
- Session/JWT — the browser.
- Better Auth
apikeys— agent-bound keys minted from web settings or MCP, CLI/Mac-app keys from the/cli-authhandshake, and the master key.
The Hermes plugin authenticates with an agent-bound API key supplied through the
INDEX_API_KEY environment variable. The Index macOS app stores an ordinary
90-day API key (from the same /cli-auth handshake the CLI uses) in the
Keychain; the key never enters JavaScript, configuration files, logs, arguments,
or browser callbacks. Revocation, expiry, and per-agent scoping come from the
apikeys table.
Scheduled negotiation runs use the hermes-negotiator audience: an API-key
principal with exactly four Hermes handlers—identity, pickup, respond,
consult—and only its precise route matrix. Pickup/respond/consult revalidate
selection, owner, agent, generation, credential row/expiry, expected speaker,
and hidden one-shot run capability under the owner transaction; one mutation has
an idempotent receipt. The server falls back to Index immediately on expiry,
stale heartbeat, pause, revocation, or authority mismatch. Fallback and cron
mutations use exact identity/generation compare-and-set fencing so a stale
relaunch, callback, or cleanup cannot alter a newer installation.
Website and optional-app owner views expose only installation, actions, health, heartbeat, expiry, fallback, pause, revoke, and reconnect state.
Direct macOS distribution targets macOS 13+, Developer ID signing, Hardened Runtime, notarization, stapling, Universal 2 artifacts, immutable production HTTPS inputs, and clean-account validation. It is not a Mac App Store product: App Sandbox is not a production requirement for this distribution model.
- Protocol package README:
packages/protocol/src/README.md— graph, agent, and tool documentation - Protocol design references:
docs/design/protocol-deep-dive.mdanddocs/design/opportunity-status-lifecycle.md - Research and historical analysis:
docs/research/ - Template files:
services/api/src/controllers/controller.template.md,services/api/src/services/service.template.md,services/api/src/queues/queue.template.md