Documentation Index
Fetch the complete documentation index at: https://docs.vertz.dev/llms.txt
Use this file to discover all available pages before exploring further.
domain() groups related entities and services under a shared namespace. Entities and services inside a domain get route-prefixed with the domain name — no manual path configuration needed.
Quick start
import { createServer, domain, entity, service } from '@vertz/server';
const invoices = entity('invoices', {
model: invoicesModel,
access: { list: rules.authenticated() },
});
const payments = service('payments', {
/* ... */
});
const billing = domain('billing', {
entities: [invoices],
services: [payments],
});
const app = createServer({ domains: [billing], db });
This generates:
GET /api/billing/invoices → list
GET /api/billing/invoices/:id → get
POST /api/billing/payments/charge → action
Without a domain, the same entity would be at /api/invoices. The domain adds the /billing/ prefix automatically.
API
function domain(name: string, config: DomainConfig): DomainDefinition;
DomainConfig
| Property | Type | Required | Description |
|---|
entities | EntityDefinition[] | No* | Entities in this domain |
services | ServiceDefinition[] | No* | Services in this domain |
middleware | NamedMiddlewareDef[] | No | Domain-scoped middleware |
* At least one of entities or services must be provided.
Name rules
Domain names must match /^[a-z][a-z0-9-]*$/:
- Start with a lowercase letter
- Only lowercase letters, digits, and hyphens
- No uppercase, no slashes, no underscores
domain('billing', { ... }) // ✅
domain('user-management', { ... }) // ✅
domain('v2-api', { ... }) // ✅
domain('Billing', { ... }) // ❌ uppercase
domain('1billing', { ... }) // ❌ starts with digit
Route prefixing
All entities and services inside a domain are prefixed with /api/{domainName}/:
// Top-level entity → /api/users
const users = entity('users', { ... });
// Domain entity → /api/billing/invoices
const invoices = entity('invoices', { ... });
const billing = domain('billing', { entities: [invoices] });
const app = createServer({
entities: [users],
domains: [billing],
db,
});
Routes generated:
GET /api/users → top-level
GET /api/billing/invoices → domain-scoped
Domain middleware
Domains can have their own middleware that runs only for routes inside that domain:
import { createMiddleware } from '@vertz/server';
const billingTracker = createMiddleware({
name: 'billing-tracker',
handler: async () => ({ billingTracked: true }),
});
const billing = domain('billing', {
entities: [invoices],
middleware: [billingTracker],
});
Execution order:
- Global middleware (from
app.middlewares([...]))
- Domain middleware (only for routes in this domain)
- Entity/service handler
Domain middleware receives the full context from global middleware (e.g., userId, tenantId). Each domain’s middleware is isolated — it doesn’t affect other domains.
Mixing domains with top-level resources
You can use domains alongside top-level entities and services:
const users = entity('users', { ... }); // top-level
const settings = entity('settings', { ... }); // top-level
const invoices = entity('invoices', { ... });
const billing = domain('billing', { entities: [invoices] });
const tasks = entity('tasks', { ... });
const projects = domain('projects', { entities: [tasks] });
const app = createServer({
entities: [users, settings],
domains: [billing, projects],
db,
});
/api/users → top-level
/api/settings → top-level
/api/billing/invoices → billing domain
/api/projects/tasks → projects domain
Cross-domain injection
Entities in one domain can reference entities from another domain using inject:
const invoices = entity('invoices', { model: invoicesModel, access: { ... } });
const tasks = entity('tasks', {
model: tasksModel,
inject: { invoices },
access: { ... },
});
const billing = domain('billing', { entities: [invoices] });
const projects = domain('projects', { entities: [tasks] });
createServer({ domains: [billing, projects], db });
The inject system resolves dependencies across domains — the entity doesn’t need to know which domain its dependency lives in.
Collision detection
The framework validates names at startup and throws clear errors for conflicts:
Duplicate domain names
const d1 = domain('billing', { entities: [invoices1] });
const d2 = domain('billing', { entities: [invoices2] });
createServer({ domains: [d1, d2] });
// ❌ Duplicate domain name "billing"
Same entity in multiple domains
const d1 = domain('billing', { entities: [invoices] });
const d2 = domain('payments', { entities: [invoices] });
createServer({ domains: [d1, d2] });
// ❌ Entity "invoices" appears in both domain "billing" and domain "payments"
Domain name conflicts with top-level resource
const billing = domain('billing', { entities: [invoices] });
const billingEntity = entity('billing', { ... });
createServer({ domains: [billing], entities: [billingEntity] });
// ❌ Domain name "billing" conflicts with top-level entity "billing"
Tenant scoping
Tenant-scoped entities (with a tenantId field) work seamlessly inside domains. The framework’s automatic tenant filtering applies regardless of domain membership:
const tasks = entity('tasks', {
model: tenantScopedModel, // has tenantId column
access: { list: rules.authenticated() },
});
const projects = domain('projects', { entities: [tasks] });
See Multi-Tenancy for more on tenant scoping.