# Modules (/docs/code/modules)

---
title: Modules
description: 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.
kind: guide
package: questpie
---

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.

```ts title="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.

```bash
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

| Module            | Import                                  | Brings                                                    |
| ----------------- | --------------------------------------- | --------------------------------------------------------- |
| `adminModule`     | `@questpie/admin/modules/admin`         | Admin routes and views, the auth collections, the sidebar |
| `auditModule`     | `@questpie/admin/modules/audit`         | An `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/workflows` | Workflow tables, jobs, routes, and a `workflows` service  |
| `mcpModule`       | `@questpie/mcp`                         | The `/api/mcp` endpoint an agent connects to              |
| `starterModule`   | `questpie`                              | The 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.

<Callout type="info" title="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.
</Callout>

## 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](/docs/code/modules/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.

```ts title="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.

| Key           | Contributes                            | Two modules, same key |
| ------------- | -------------------------------------- | --------------------- |
| `name`        | The identifier, and the de-dup key     | Last one wins         |
| `modules`     | Dependencies, resolved before this one | Flattened in          |
| `collections` | Collections, keyed by name             | Override by key       |
| `globals`     | Globals, keyed by name                 | Override by key       |
| `routes`      | Routes, keyed by path                  | Override by key       |
| `jobs`        | Job definitions, keyed by name         | Override by key       |
| `services`    | Services, keyed by name                | Override by key       |
| `channels`    | Realtime channels                      | Override by key       |
| `fields`      | Field factories, from its `fields.ts`  | Override by key       |
| `migrations`  | Migrations                             | Both run              |
| `seeds`       | [Seeds](/docs/schema/seeds)            | Both run              |
| `messages`    | Backend messages, keyed by locale      | Merged per locale     |
| `config`      | One key per `config/*.ts` file         | Merged per key        |
| `plugin`      | A codegen plugin, or an array of them  | Codegen 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.

<Callout type="warn" title="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.
</Callout>

## 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.

```ts title="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](/docs/schema/collections#merging-two-builders).

## 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

| Topic                                        | Page                                             |
| -------------------------------------------- | ------------------------------------------------ |
| Resolution order, and the rule for every key | [Merging](/docs/code/modules/merging)            |
| Shipping a module as an npm package          | [Publishing](/docs/code/modules/publishing)      |
| `modules.ts` beside the other config files   | [Configuration](/docs/ship/configuration)        |
| Writing the `CodegenPlugin` a module ships   | [Building a plugin](/docs/guides/build-a-plugin) |
| Extending a collection a module ships        | [Collections](/docs/schema/collections)          |

## Next

**[Codegen](/docs/code/codegen)** is the pipeline that reads `modules.ts`,
folds every module into one app, and writes the types you import.
