QUESTPIE
Code

Modules

A module is a plain object holding collections, routes, jobs and config. Add it to modules.ts and the whole feature arrives typed, in one line. QUESTPIE ships its own batteries this way, and you publish yours the same way.

View markdown

Every app has a modules.ts. Codegen throws without one. This page is what goes in that file, and what to write when the feature you want is your own.

Install one

modules.ts sits beside your collections/ directory. It default-exports an array. Import a module and put it in.

src/questpie/server/modules.ts
import { adminModule } from "@questpie/admin/modules/admin";
import { openApiModule } from "@questpie/openapi";

export default [adminModule, openApiModule] as const;

Then regenerate.

questpie generate

That is the whole install. You get the admin panel, the auth collections behind it, and the Scalar reference at /api/docs. You did not touch questpie.config.ts. There is no second list of plugins to keep in sync.

What QUESTPIE ships

ModuleImportBrings
adminModule@questpie/admin/modules/adminAdmin routes and views, the auth collections, the sidebar
auditModule@questpie/admin/modules/auditAn admin_audit_log collection and a cleanup job
openApiModule@questpie/openapi/api/openapi.json and the Scalar page at /api/docs
workflowsModule@questpie/workflows/modules/workflowsWorkflow tables, jobs, routes, and a workflows service
mcpModule@questpie/mcpThe /api/mcp endpoint an agent connects to
starterModulequestpieThe auth, OAuth and assets tables, with no admin panel

adminModule already depends on starterModule, so listing both is redundant. Reach for starterModule when you want auth tables and no admin UI.

An empty array is still a working app

createApp prepends questpie-core unless you listed it yourself. That module carries the CRUD routes, the built-in field types, and the services behind db, storage, queue and auth. What it does not carry is a user table. That one lives in the starter.

Write one

A module is a plain object, and there are two ways to arrive at one.

Write the object yourself, which the rest of this section shows. Or lay the module out as directories and let codegen build the object, which Publishing covers. Both produce the same shape, so nothing downstream can tell them apart.

Pick by size. The object form suits a module you can read in one screen. Every module QUESTPIE ships uses the directory form, because core alone carries three jobs and more than a dozen routes.

module() is an identity function. It returns your object unchanged, so it costs nothing at runtime. It is there to check the shape and carry your object's type forward.

src/questpie/server/blog-module.ts
import { module } from "questpie/app";

import { collection } from "#questpie/factories";

export const blogModule = module({
	name: "blog", // unique, this is the de-dup key
	collections: {
		posts: collection("posts")
			.fields(({ f }) => ({ title: f.text(255).required() }))
			.title(({ f }) => f.title),
	},
	messages: { en: { "blog.published": "Published" } },
});

Add blogModule to the modules.ts array and run questpie generate. app.collections.posts now exists, with the types a file under collections/ would have given it.

Import collection from #questpie/factories, not from questpie. The generated factory is the one that knows your enabled modules, so module field types like f.richText() appear on f. module comes from questpie/app. That barrel does not re-export collection.

What a module can carry

name is the only required key. The last column is what happens when two modules use the same one.

KeyContributesTwo modules, same key
nameThe identifier, and the de-dup keyLast one wins
modulesDependencies, resolved before this oneFlattened in
collectionsCollections, keyed by nameOverride by key
globalsGlobals, keyed by nameOverride by key
routesRoutes, keyed by pathOverride by key
jobsJob definitions, keyed by nameOverride by key
servicesServices, keyed by nameOverride by key
channelsRealtime channelsOverride by key
fieldsField factories, from its fields.tsOverride by key
migrationsMigrationsBoth run
seedsSeedsBoth run
messagesBackend messages, keyed by localeMerged per locale
configOne key per config/*.ts fileMerged per key
pluginA codegen plugin, or an array of themCodegen only

The interface ends in an index signature, so a package can add keys of its own. @questpie/admin uses that for views, components and blocks. Those merge like any other record. You never write those keys by hand. Codegen fills them from the package's own files.

A module holds finished definitions

Put collections, jobs and services straight into the object. Nothing inside a module runs. Work that has to happen at boot belongs in a service lifecycle.

Your files win

Modules resolve first. Your own collections/, routes/ and the rest are folded in last. So a key you define replaces the module's version outright.

That is the override seam, and it is also the trap. Redefining user from scratch drops the auth columns and the admin config the starter set up. Extend it instead. .merge() folds another builder of the same name into yours.

src/questpie/server/collections/user.ts
import { starterModule } from "questpie";

import { collection } from "#questpie/factories";

export const user = collection("user")
	.merge(starterModule.collections.user)
	.fields(({ f }) => ({ bio: f.textarea() }));

@questpie/admin uses the same call to lay its UI config over the starter's collections. See Collections.

The plugin key is different

plugin is the one key that never reaches the running app. Codegen reads it in a pre-pass, then drops it before the merge.

That separation is why one array is enough. A package with file conventions of its own puts its CodegenPlugin on the module. Adding the module registers the entities and the conventions together.

Where each topic lives

TopicPage
Resolution order, and the rule for every keyMerging
Shipping a module as an npm packagePublishing
modules.ts beside the other config filesConfiguration
Writing the CodegenPlugin a module shipsBuilding a plugin
Extending a collection a module shipsCollections

Next

Codegen is the pipeline that reads modules.ts, folds every module into one app, and writes the types you import.

On this page