Skip to content

Architecture Overview

Tech Stack

LayerTechnology
HTTP FrameworkFastify 5
LanguageTypeScript (ESM)
Database ORMKnex.js
Primary DatabasePostgreSQL
Also SupportedMySQL / MySQL2, SQLite3, better-sqlite3
ValidationZod
TestingVitest
CachingIn-memory or Redis
File StorageLocal disk or S3-compatible

Project Structure

service/api/src/
├── auth/               # Auth strategies (JWT, session, sub-token, SSO drivers)
│   ├── drivers/        # OAuth2, OpenID, SAML, Local
│   ├── sso/            # SSO config & validation
│   ├── session.ts      # Session DB helpers
│   ├── session-cache.ts# Redis/memory session cache
│   └── token.ts        # JWT sign/verify
├── cache/              # Caching layer (memory/redis)
├── cli/                # CLI commands (bootstrap, migrate, etc.)
├── database/           # Knex connection, migrations, schema helpers
├── extensions/         # Extension loading and hooks
├── middleware/         # Fastify preHandler hooks
│   ├── authenticate.ts # 4-strategy auth pipeline
│   ├── extract-token.ts# Token extraction from header/cookie/query
│   ├── sanitize-query.ts# Query parameter sanitization
│   ├── error-handler.ts# Global error formatter
│   ├── require-admin.ts
│   ├── require-app-access.ts
│   └── collection-exists.ts
├── modules/            # Self-contained feature modules
│   ├── notify/         # Notification engine
│   ├── workflow/       # Workflow engine
│   ├── mcp/            # MCP integration
│   └── metrics/        # Prometheus metrics
├── permissions/        # RBAC logic
├── routes/             # Fastify route handlers (thin controllers)
├── services/           # Business logic (thick services)
├── storage/            # File storage drivers
├── types/              # TypeScript interfaces
└── utils/              # Shared helpers, errors

Request Lifecycle

Middleware Pipeline

Every request passes through these preHandlers (registered globally in server.ts):

  1. extractToken — Reads the bearer token from Authorization header, access_token query param, or session cookie.
  2. authenticate — Resolves the request.accountability object using the 4-strategy pipeline. Falls back to public access.
  3. sanitizeQuery — Parses and validates query parameters (fields, filter, sort, limit, offset, deep, aggregate, meta). Uses qs for deep object parsing.
  4. schema — (route-level) Attaches the current database schema overview to request.schema.

After these preHandlers, route-level preHandlers may add:

  • requireAdmin — Throws ForbiddenError if accountability.admin is false.
  • requireAppAccess — Throws ForbiddenError if accountability.app is false.
  • collectionExists — Validates the :collection param exists in the schema.
  • validateBatch — Ensures batch operations have valid keys or query.

Service Layer Pattern

Routes are thin controllers. All business logic lives in services.

typescript
// Route (thin)
app.get('/items/:collection', { preHandler: [collectionExists] }, async (request, reply) => {
  const service = new ItemsService(request.collection!, {
    accountability: request.accountability,
    schema: request.schema,
  });
  const items = await service.readByQuery(request.sanitizedQuery);
  return reply.send({ data: items });
});

// Service (thick)
class ItemsService {
  constructor(collection: string, opts: ServiceOptions) { ... }
  async readByQuery(query: Query): Promise<Item[]> {
    // Check permissions, build Knex query, apply field-level filters, return data
  }
}

Services always receive:

  • accountability — who is making the request (used for permission checks)
  • schema — current database schema (collections, fields, relations)

Accountability Object

The Accountability interface is the core security context passed through every service call:

typescript
interface Accountability {
  user: string | null;       // User UUID (null = public)
  role: string | null;       // Primary role UUID
  roles: string[];           // All effective role UUIDs
  admin: boolean;            // Bypass all permission checks
  tech: boolean;             // System/debug access (implies admin)
  app: boolean;              // App panel access
  ip: string | null;
  userAgent: string | null;
  origin: string | null;
  // Impersonation
  impersonated_by?: string | null;
  impersonation_session?: string | null;
  is_impersonating?: boolean;
  // Sub-token
  sub_token_id?: string | null;
  scopes?: string[] | null;  // Allowed actions for sub-token
}

Error Handling

See Error Handling Guide.

Modules

Modules are self-contained feature packages under src/modules/. Each module has its own:

  • routes.ts — Fastify plugin registering module endpoints
  • services/ — Business logic services
  • types.ts — TypeScript interfaces
  • validation.ts — Zod schemas

Modules are registered in server.ts and mounted under their own URL prefixes (e.g., /notify/*, /workflow*).

Extension System

Extensions can hook into the request lifecycle via the emitter (event bus):

  • emitter.emitFilter('authenticate', ...) — Override authentication
  • emitter.emitFilter('request.error', ...) — Modify error responses
  • emitter.emitAction('items.create', ...) — React to data mutations

Extensions are loaded from EXTENSIONS_PATH (default: ./extensions).

ODP Internal API Documentation