Skip to content

Schema file shape

A schema file is a CommonJS module that exports a single plain object. Drop one under schema/versions/v1/, and the loader builds every surface from it. The loader walks each file once at boot (and on every change in dev) — see Schema-driven generation for the mechanics.

module.exports = {
path: 'account',
collection: 'account',
fields: [
{ name: 'userId', type: String, required: true },
{ name: 'name', type: String, required: true },
],
};

That’s enough to mount REST routes, GraphQL types, MCP tools, and Swagger fragments for account.

KeyTypeRequiredWhat it does
pathstringyesURL segment under /api/v1/<path>, GraphQL prefix, MCP tool prefix. Must be unique across loaded schemas.
collectionstringyesMongoDB collection name. Conventionally matches path.
fieldsarrayyesField definitions — see Field options.
relationsobjectnoRelation graph (belongsTo / hasOne / hasMany) consumed by __include and per-relation MCP tools. See Relations.
aggregationsarraynoDeclarative aggregation pipelines that surface as REST + GraphQL + MCP. See Aggregations.
compositeIndexarraynoArray of Mongo index specs — applied with unique: true. Use for per-tenant uniqueness (e.g. { userId: 1, slug: 1 }).
softDeletebooleannoDefaults to true. Set false to opt out of tombstones — DELETEs become hard-deletes. See Soft delete.
auditbooleannoDefaults to true. Set false to skip audit log writes for this schema. See Audit log.
aclobjectnoDocument-level role bypass slots (list, delete). See ACL.
webhooksobjectnoOutbound webhook subscriptions for create / update / delete events on this schema. See Webhooks.
softDeleteobjectno{ retentionDays: N } to auto-purge tombstoned rows after N days. Without it, tombstones live forever. See Backup & retention.
versionstringnoDefaults to v1. Set when you want a single schema under a non-default version segment.

Anything else on the top-level object is ignored — there’s no escape hatch for runtime config from the schema file. Code-level extensions go in app.js (custom routes) or utils/ (cross-cutting helpers); see Where to put new code.

fields is an array (order matters for Swagger). Each entry is an object — see Field options for the full vocabulary. The two ownership fields — userId and accountId — have special status: any schema can declare them, the framework stamps them from the JWT, and clients can never supply them. See Tenant isolation.

relations: {
// belongsTo: this schema holds the foreign key.
account: { belongsTo: 'account', fk: 'accountId' },
// hasMany: the OTHER schema holds the foreign key.
contacts: { hasMany: 'contact', fk: 'accountId' },
// hasOne: same as hasMany but at most one match.
primaryAddress: { hasOne: 'address', fk: 'accountId', where: { isPrimary: true } },
}
Key under each relationDescription
belongsTo / hasOne / hasManyExactly one of these names the target schema’s path.
fkForeign key field. For belongsTo, it lives on this schema; for hasOne / hasMany, on the target. Defaults to <target>Id.
whereOptional filter applied to the target query. Useful for hasOne selection.

field.reference is a legacy shorthand for belongsTo; new code should prefer the relations map. See Relations.

aggregations: [
{
name: 'countByStage',
description: 'Quote count grouped by stage for the authenticated user.',
pipeline: [
{ $group: { _id: '$stage', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
],
cache: { ttlSeconds: 30 },
params: [
{ name: 'since', type: 'date', match: { createdAt: { $gte: '$since' } } },
],
},
],
KeyDescription
nameUnique within the schema. Becomes the URL segment and the GraphQL field.
descriptionSurfaces in _describe and Swagger.
pipelineMongo aggregation pipeline. The framework prepends $match: { userId } automatically — even unsafe: true aggregations cannot bypass tenant scoping.
cache.ttlSecondsOptional in-process cache (per-tenant key).
paramsOptional declarative inputs. Each becomes typed in _describe, the GraphQL args, and the MCP tool input schema.
unsafeSet true to expose to operators with acl.list only. The tenant scope still applies.

See Aggregations for the param syntax.

Each entry is passed to Mongoose’s index() with unique: true:

compositeIndex: [
{ userId: 1, slug: 1 }, // per-tenant slug uniqueness
{ userId: 1, accountId: 1, year: 1, number: 1 }, // per-tenant invoice numbering
],

Always include userId (or accountId for the org variant) as the first key — without it, you’ve created a global uniqueness constraint that crosses tenants.

Default true. The framework adds a deletedAt: Date tombstone field, rewrites every list / get query with deletedAt: null, and turns DELETE into “set the tombstone.” It also generates a POST /:id/restore REST route, a restore_<path> MCP tool, and a <path>RemoveById GraphQL mutation that respects the same.

softDelete: false // hard-delete on DELETE; no `deletedAt` field

See Soft delete.

Default true. Every create / update / delete / restore / state-machine transition writes a row to the audit_log collection with before / after / diff projections. Reads come back as a history_<path> MCP tool, a historyByDoc GraphQL field, and GET /:id/history REST.

audit: false // no audit rows written; history endpoints absent

See Audit log.

Document-level bypass slots — opt operators in to see / delete across tenants. Field-level ACL goes on the field, not here.

acl: {
list: ['admin', 'support'],
delete: ['admin'],
},

See ACL.

webhooks: {
events: ['created', 'updated', 'deleted', 'transitioned'],
endpoints: [
{ url: 'https://hooks.example.com/davepi', secret: 'whsec_...' },
],
}

The framework signs each delivery with HMAC-SHA256, retries with exponential backoff, and emits an audit row per attempt. See Webhooks.

softDelete: { retentionDays: 30 }

Opt in to auto-purge of soft-deleted rows after N days. Without this (and no SOFT_DELETE_RETENTION_DAYS env var), tombstones live forever. Audit log and webhook delivery rows aren’t auto-purged at all — manual cron, see Backup & retention.

See Backup & retention.

These are deliberate gaps — the schema file describes data, not runtime:

  • Custom routes: add them in app.js after the schemas.forEach loop.
  • Auth flows: routes/auth/ is hand-written.
  • Custom middleware: middleware/.
  • Non-Mongo backends: not supported. dAvePi is Mongo-only by design.