# Backend Architecture

This document describes the architectural patterns and design principles that govern the Semiont backend.

## Infrastructure Management

### Centralized Infrastructure Pattern

**All infrastructure components are created once and managed by MakeMeaningService.**

This is a **critical architectural constraint** that must be followed throughout the backend codebase.

#### The Rule

```typescript
// ✅ CORRECT: Access infrastructure via MakeMeaningService
const { eventStore, graphDb, repStore, inferenceClient } = c.get('makeMeaning');

// ❌ WRONG: NEVER create infrastructure in routes or services
const graphDb = await getGraphDatabase(config);              // VIOLATION
const repStore = new FilesystemRepresentationStore(...);     // VIOLATION
const eventStore = createEventStore(...);                    // VIOLATION
const inferenceClient = await getInferenceClient(config);   // VIOLATION
```

#### What MakeMeaningService Owns

The `MakeMeaningService` created in [src/index.ts:56](../src/index.ts#L56) via `startMakeMeaning()` owns **all infrastructure**.

See [@semiont/make-meaning](../../../packages/make-meaning/) for the implementation of `startMakeMeaning()` and detailed infrastructure ownership documentation.

**Infrastructure Components:**

1. **EventStore** - Event log and materialized views
   - Single source of truth for all data
   - Manages event subscription and view materialization
   - Created once, shared across entire application

2. **GraphDatabase** - Graph database connection
   - Provides relationship traversal and backlinks
   - Single connection pool shared across requests
   - Automatically synchronized via GraphDBConsumer

3. **RepresentationStore** - Content-addressed document storage
   - Stores all document content using content hashing
   - Deduplication and efficient retrieval
   - Single instance prevents duplicate file operations

4. **InferenceClient** - LLM inference client
   - Connection to AI model provider (OpenAI, Anthropic, etc.)
   - Request pooling and rate limiting
   - Shared configuration and API keys

5. **JobQueue** - Background job processing
   - Filesystem-based job queue behind a `JobQueue` interface
   - Exposes three HTTP endpoints for the worker pool (see below)

6. **Worker Pool** - Separate process running 6 worker types
   - ReferenceDetectionWorker
   - GenerationWorker
   - HighlightDetectionWorker
   - AssessmentDetectionWorker
   - CommentDetectionWorker
   - TagDetectionWorker
   - Workers connect to the KS over HTTP/SSE via WorkerStateUnit (from @semiont/api-client), not via the in-process EventBus

7. **GraphDBConsumer** - Event-to-graph synchronization
   - Subscribes to event store
   - Updates graph database on resource/annotation changes
   - Maintains graph consistency

#### Implementation Pattern

**Backend Initialization** ([src/index.ts:56](../src/index.ts#L56)):

```typescript
// Create MakeMeaningService ONCE at startup
const makeMeaning = await startMakeMeaning(config);

// Inject into Hono context for all routes
app.use('*', async (c, next) => {
  c.set('makeMeaning', makeMeaning);
  await next();
});
```

**Route Pattern**:

```typescript
router.get('/resources/:id', async (c) => {
  // ✅ Get infrastructure from context
  const { eventStore, graphDb, repStore } = c.get('makeMeaning');

  // Use infrastructure
  const resource = await graphDb.getResource(resourceId);
  const content = await repStore.retrieve(checksum, mediaType);
  await eventStore.appendEvent(event);

  return c.json(response);
});
```

**Service Pattern** (Dependency Injection):

```typescript
// Service receives infrastructure as parameters
export class ResourceOperations {
  static async createResource(
    input: CreateResourceInput,
    user: User,
    eventStore: EventStore,        // Injected
    repStore: RepresentationStore,  // Injected
    config: EnvironmentConfig
  ): Promise<CreateResourceResponse> {
    // Use injected infrastructure
    const storedRep = await repStore.store(content, metadata);
    await eventStore.appendEvent(event);
  }
}

// Route calls service with infrastructure from context
router.post('/resources', async (c) => {
  const { eventStore, repStore } = c.get('makeMeaning');
  const response = await ResourceOperations.createResource(
    input,
    user,
    eventStore,  // Pass from context
    repStore,    // Pass from context
    config
  );
  return c.json(response);
});
```

#### Why This Pattern Matters

**1. Prevents Resource Leaks**
- Single database connection instead of one per request
- No duplicate file handles or network sockets
- Controlled lifecycle with `makeMeaning.stop()`

**2. Ensures Consistent State**
- All components use the same configuration
- No configuration drift between instances
- Centralized connection pooling

**3. Simplifies Testing**
- Single injection point for mocking
- Consistent test setup across all tests
- Easy to swap implementations

**4. Clear Ownership**
- No ambiguity about who creates/destroys resources
- Explicit dependency flow through constructor/parameters
- Easier to reason about system initialization

**5. Performance**
- Connection pooling and reuse
- Shared caches across requests
- Reduced initialization overhead

#### Verification

To verify compliance with this pattern:

```bash
# Check for violations in routes
grep -r "new FilesystemRepresentationStore\|await getGraphDatabase\|await getInferenceClient\|createEventStore(" \
  apps/backend/src/routes --include="*.ts"

# Check for violations in services
grep -r "new FilesystemRepresentationStore\|await getGraphDatabase\|await getInferenceClient\|createEventStore(" \
  apps/backend/src/services --include="*.ts"

# Should return no results (all matches should be in test files only)
```

#### Common Violations and Fixes

**❌ Violation 1: Creating GraphDatabase in route**
```typescript
// WRONG
router.get('/resources/:id/references', async (c) => {
  const graphDb = await getGraphDatabase(config);  // VIOLATION
  const refs = await graphDb.getResourceReferences(id);
});
```

**✅ Fix: Access from context**
```typescript
// CORRECT
router.get('/resources/:id/references', async (c) => {
  const { graphDb } = c.get('makeMeaning');  // ✅
  const refs = await graphDb.getResourceReferences(id);
});
```

**❌ Violation 2: Creating RepresentationStore in service**
```typescript
// WRONG
class MyService {
  static async process(input: Input, config: EnvironmentConfig) {
    const repStore = new FilesystemRepresentationStore(...);  // VIOLATION
    await repStore.store(content, metadata);
  }
}
```

**✅ Fix: Accept as parameter**
```typescript
// CORRECT
class MyService {
  static async process(
    input: Input,
    repStore: RepresentationStore,  // Injected parameter
    config: EnvironmentConfig
  ) {
    await repStore.store(content, metadata);
  }
}

// Route passes from context
router.post('/process', async (c) => {
  const { repStore } = c.get('makeMeaning');
  await MyService.process(input, repStore, config);  // ✅
});
```

**❌ Violation 3: Creating InferenceClient for one-off use**
```typescript
// WRONG
async function generateSummary(text: string, config: EnvironmentConfig) {
  const client = await getInferenceClient(config);  // VIOLATION
  return await client.generateText(prompt);
}
```

**✅ Fix: Accept as parameter**
```typescript
// CORRECT
async function generateSummary(
  text: string,
  inferenceClient: InferenceClient,  // Injected parameter
  config: EnvironmentConfig
) {
  return await inferenceClient.generateText(prompt);
}

// Route passes from context
router.post('/summarize', async (c) => {
  const { inferenceClient } = c.get('makeMeaning');
  const summary = await generateSummary(text, inferenceClient, config);  // ✅
});
```

## KS/Worker Process Split

The backend runs as two processes:

1. **Knowledge System (KS)** -- the main process. Runs all KB actors (Stower, Browser, Gatherer, Matcher, Smelter), all stores, the RxJS EventBus, SSE streaming, the HTTP API, and the job queue.

2. **Worker Pool** -- a separate process external. Runs the Generator and the five annotation detection workers. Workers do not share the in-process EventBus. Instead, they use `WorkerStateUnit` from `@semiont/api-client` to connect to the KS over HTTP and SSE -- the same transport the frontend uses.

### Worker Endpoints

The KS exposes three endpoints for worker communication:

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/jobs/stream?type=...` | GET (SSE) | Push job assignments to workers as they become available |
| `/jobs/:id/claim` | POST | Atomic job claim -- ensures exactly one worker processes each job |
| `/jobs/:id/events` | POST | Workers emit domain events back to the KS EventBus |

### Why Two Processes

- CPU-heavy LLM inference and annotation detection run in a separate V8 isolate, keeping the KS event loop responsive to user requests.
- Workers are stateless with respect to the KB. They receive job assignments, do inference, and emit events back. All durable state lives in the KS.
- The worker pool can crash and restart without affecting the KS or connected frontend clients.

### Job Queue

The job queue is filesystem-based, behind a `JobQueue` interface that allows future backing store swaps (Postgres, Redis, etc.) without changing the HTTP contract or the SDK. Jobs are created by the KS, streamed to workers via SSE, and completed via the claim/events endpoints above.

## EventBus-Delegated Routes

All knowledge-domain read routes delegate to the EventBus rather than accessing KB stores directly. This makes the EventBus a complete interface for all system behavior — HTTP is just one transport.

### Pattern

Routes use the `eventBusRequest()` helper from `src/utils/event-bus-request.ts`:

```typescript
import { eventBusRequest } from '../utils/event-bus-request';

router.get('/resources', async (c) => {
  const eventBus = c.get('eventBus');
  const correlationId = crypto.randomUUID();

  const response = await eventBusRequest(
    eventBus,
    'browse:resources-requested',    // emit this event
    { correlationId, limit: 20 },    // with this payload
    'browse:resources-result',       // await this success event
    'browse:resources-failed',       // or this failure event
  );
  return c.json(response);
});
```

The helper:
1. Subscribes to success and failure events filtered by `correlationId`
2. Emits the request event
3. Returns the `response` field from the success event, or throws the `error` from failure
4. Times out after 30 seconds

### What Delegates to EventBus

| Route | Event | Actor |
|-------|-------|-------|
| `GET /resources` | `browse:resources-requested` | Gatherer |
| `GET /resources/:id` (JSON-LD) | `browse:resource-requested` | Gatherer |
| `GET /resources/:id/annotations` | `browse:annotations-requested` | Gatherer |
| `GET /resources/:resourceId/annotations/:annotationId` | `browse:annotation-requested` | Gatherer |
| `GET /resources/:id/events` | `browse:events-requested` | Gatherer |
| `GET /resources/:resourceId/annotations/:annotationId/history` | `browse:annotation-history-requested` | Gatherer |
| `GET /resources/:id/referenced-by` | `bind:referenced-by-requested` | Matcher |
| `GET /api/entity-types` | `mark:entity-types-requested` | Gatherer |
| `GET /api/jobs/:id` | `job:status-requested` | Job subscription |
| `GET /resources/:id/llm-context` | `gather:resource-requested` | Gatherer |
| `GET /.../llm-context` (annotation) | `gather:requested` | Gatherer |
| `POST /resources/:id/clone-with-token` | `yield:clone-token-requested` | CloneTokenManager |
| `GET /api/clone-tokens/:token` | `yield:clone-resource-requested` | CloneTokenManager |
| `POST /api/clone-tokens/create-resource` | `yield:clone-create` | CloneTokenManager |

### What Stays HTTP-Only

- **Auth routes** — PostgreSQL/Prisma, JWT, OAuth (orthogonal to knowledge domain)
- **Admin routes** — PostgreSQL/Prisma (orthogonal to knowledge domain)
- **Health/Status** — infrastructure monitoring
- **Binary content** — `GET /resources/:id` with `Accept: text/*`, `image/*`, `application/pdf`
- **Resource upload** — `POST /resources` with multipart form data

## Related Documentation

- [Make-Meaning Package](../../../packages/make-meaning/) - Implementation of MakeMeaningService
- [Event Sourcing Package](../../../packages/event-sourcing/) - EventStore implementation
- [Graph Package](../../../packages/graph/) - GraphDatabase implementation
- [Content Package](../../../packages/content/) - RepresentationStore implementation
- [Inference Package](../../../packages/inference/) - InferenceClient implementation

---

**Last Updated**: 2026-03-11
