Schema-driven generation
dAvePi’s central idea: the schema file is the source of truth, and
everything else is a projection. Drop a file under
schema/versions/v1/, and the framework generates:
- Mongoose model — registered with the connection, indexes built
- REST routes —
POST,GET(list),PUT(bulk),GET / PUT / DELETE /:id,restore,history,aggregations, file upload routes - GraphQL types — output type, input types (writable / partial /
filter), every standard resolver via
graphql-compose-mongoose - MCP tools — one per CRUD operation per schema, plus per-aggregation and per-relation tools
- Swagger fragment — paths and definitions wired into the live spec
- Admin SPA resource — Refine reads the
_describemanifest at startup and renders forms / tables / detail views automatically - Typed client output —
davepi gen-clientwalks the schema map and emits per-resource interfaces and method signatures
module.exports = { path: 'account', collection: 'account', fields: [ { name: 'userId', type: String, required: true }, { name: 'name', type: String, required: true, searchable: true }, ],};That’s all you write. The list above happens at boot.
Where the generation lives
Section titled “Where the generation lives”utils/schemaLoader.js walks schema/versions/* once at startup. For
each schema it:
- Builds a Mongoose schema with timestamps, soft-delete tombstone, composite indexes, full-text index for searchable fields.
- Composes a Mongoose-derived TC in graphql-compose’s registry,
wraps every standard resolver with
wrapFilter/wrapByIdMutation/ etc. so tenant scoping is non-bypassable. - Mounts a per-schema Express Router carrying every REST route.
- Emits a Swagger fragment into the live
apiSpecobject served by/api-docs/swagger.json. - Registers MCP tools via
utils/mcpServer.jsagainst the same registry.
Per-schema routers are mounted on the parent app via app.use(router),
so a schema unload (during hot-reload) splices its router out without
rebuilding the whole stack.
Hot reload
Section titled “Hot reload”In dev (HOT_RELOAD_SCHEMAS=true), a chokidar watcher fires schema
add / change / unlink events. Each event runs through a single-flight
queue (opChain in the loader), so concurrent file changes don’t
interleave registry mutations with rebuildGraphQL. The Apollo router
swap uses an indirection middleware: the parent app holds a let apolloRouter, the loader replaces it on rebuild, in-flight requests
hit the previous router, new requests hit the new one.
See Hot reload for the full mechanism.
Why one source of truth matters
Section titled “Why one source of truth matters”Every surface stays in lockstep automatically. Add a field:
- The REST
POSTbody schema knows about it. - The GraphQL
inputtype accepts it. - The MCP
create_<path>tool’s input schema validates it. - The Swagger UI shows it.
- The admin SPA renders a form input for it.
- The next
gen-clientrun emits a TS field for it.
No backend / frontend drift. No “I forgot to update the GraphQL schema when I added the field.” No mismatch between the OpenAPI spec and what the server actually accepts.
What you give up
Section titled “What you give up”- Schema-flexibility tax: every resource looks the same shape
(CRUD + tenancy + soft-delete). For a wildly bespoke surface, you’d
add custom routes after the schema loop in
app.js. - MongoDB only: the framework is built on Mongoose; SQL backends aren’t supported.
- One ownership column:
userIdis the tenant column on every schema. Multi-org / multi-team models layer on top viaaccountIdor custom relations — see Tenant isolation.