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.
Minimum viable schema
Section titled “Minimum viable schema”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.
Top-level keys
Section titled “Top-level keys”| Key | Type | Required | What it does |
|---|---|---|---|
path | string | yes | URL segment under /api/v1/<path>, GraphQL prefix, MCP tool prefix. Must be unique across loaded schemas. |
collection | string | yes | MongoDB collection name. Conventionally matches path. |
fields | array | yes | Field definitions — see Field options. |
relations | object | no | Relation graph (belongsTo / hasOne / hasMany) consumed by __include and per-relation MCP tools. See Relations. |
aggregations | array | no | Declarative aggregation pipelines that surface as REST + GraphQL + MCP. See Aggregations. |
compositeIndex | array | no | Array of Mongo index specs — applied with unique: true. Use for per-tenant uniqueness (e.g. { userId: 1, slug: 1 }). |
softDelete | boolean | no | Defaults to true. Set false to opt out of tombstones — DELETEs become hard-deletes. See Soft delete. |
audit | boolean | no | Defaults to true. Set false to skip audit log writes for this schema. See Audit log. |
acl | object | no | Document-level role bypass slots (list, delete). See ACL. |
webhooks | object | no | Outbound webhook subscriptions for create / update / delete events on this schema. See Webhooks. |
softDelete | object | no | { retentionDays: N } to auto-purge tombstoned rows after N days. Without it, tombstones live forever. See Backup & retention. |
version | string | no | Defaults 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
Section titled “fields”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
Section titled “relations”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 relation | Description |
|---|---|
belongsTo / hasOne / hasMany | Exactly one of these names the target schema’s path. |
fk | Foreign key field. For belongsTo, it lives on this schema; for hasOne / hasMany, on the target. Defaults to <target>Id. |
where | Optional 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
Section titled “aggregations”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' } } }, ], },],| Key | Description |
|---|---|
name | Unique within the schema. Becomes the URL segment and the GraphQL field. |
description | Surfaces in _describe and Swagger. |
pipeline | Mongo aggregation pipeline. The framework prepends $match: { userId } automatically — even unsafe: true aggregations cannot bypass tenant scoping. |
cache.ttlSeconds | Optional in-process cache (per-tenant key). |
params | Optional declarative inputs. Each becomes typed in _describe, the GraphQL args, and the MCP tool input schema. |
unsafe | Set true to expose to operators with acl.list only. The tenant scope still applies. |
See Aggregations for the param syntax.
compositeIndex
Section titled “compositeIndex”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.
softDelete
Section titled “softDelete”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` fieldSee 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 absentSee 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
Section titled “webhooks”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 }
Section titled “softDelete: { retentionDays }”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.
What you can’t do from a schema file
Section titled “What you can’t do from a schema file”These are deliberate gaps — the schema file describes data, not runtime:
- Custom routes: add them in
app.jsafter theschemas.forEachloop. - Auth flows:
routes/auth/is hand-written. - Custom middleware:
middleware/. - Non-Mongo backends: not supported. dAvePi is Mongo-only by design.
See also
Section titled “See also”- Field options — every key inside a
fields[]entry. - Conventions — naming, what to put where, what to avoid.
- Tenant isolation — why
userIdis special.