Appearance
Architecture Overview
Tech Stack
| Layer | Technology |
|---|---|
| HTTP Framework | Fastify 5 |
| Language | TypeScript (ESM) |
| Database ORM | Knex.js |
| Primary Database | PostgreSQL |
| Also Supported | MySQL / MySQL2, SQLite3, better-sqlite3 |
| Validation | Zod |
| Testing | Vitest |
| Caching | In-memory or Redis |
| File Storage | Local 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, errorsRequest Lifecycle
Middleware Pipeline
Every request passes through these preHandlers (registered globally in server.ts):
extractToken— Reads the bearer token fromAuthorizationheader,access_tokenquery param, or session cookie.authenticate— Resolves therequest.accountabilityobject using the 4-strategy pipeline. Falls back to public access.sanitizeQuery— Parses and validates query parameters (fields,filter,sort,limit,offset,deep,aggregate,meta). Usesqsfor deep object parsing.schema— (route-level) Attaches the current database schema overview torequest.schema.
After these preHandlers, route-level preHandlers may add:
requireAdmin— ThrowsForbiddenErrorifaccountability.adminis false.requireAppAccess— ThrowsForbiddenErrorifaccountability.appis false.collectionExists— Validates the:collectionparam exists in the schema.validateBatch— Ensures batch operations have validkeysorquery.
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 endpointsservices/— Business logic servicestypes.ts— TypeScript interfacesvalidation.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 authenticationemitter.emitFilter('request.error', ...)— Modify error responsesemitter.emitAction('items.create', ...)— React to data mutations
Extensions are loaded from EXTENSIONS_PATH (default: ./extensions).