Merging
Every module you list is flattened into one ordered list, then folded left to right into a single app. This is that order, and the rule each key follows when two modules touch it.
The rule for each key
Two modules can contribute the same key. What happens then depends on the key, not on the modules.
| Key | Rule |
|---|---|
collections | Spread merge. The later module's entry wins per key. |
globals | Spread merge, per key. |
routes | Spread merge, per key. |
jobs | Spread merge, per key. |
services | Spread merge, per key. |
channels | Spread merge, per key. |
fields | Spread merge, per key. |
migrations | Concatenated. Both run. |
seeds | Concatenated. Both run. |
messages | Merged per locale. A later key wins, not the whole locale. |
config | Merged per config key. See below. |
name, modules, plugin | Skipped. They are structure, not data. |
| anything else | Arrays concatenate, objects spread, otherwise the later value wins. |
That last row is what carries a package's own keys. @questpie/admin
contributes views and components as records, so they spread like the rows
above them.
The order
The list is built before anything merges.
questpie-coregoes first, unless you listed it yourself.- Then your
modules.tsarray, in the order you wrote it. - Then one implicit module holding everything codegen found in your own directories.
Each entry is expanded before it is added. A module's own modules array is
flattened in front of it, recursively. So dependencies always sit to the left
of the module that named them.
For export default [adminModule, openApiModule] the resolved order is:
questpie-core → questpie-oauth → questpie-starter → questpie-admin
→ questpie-openapi → your filesThose first three are not in your array. adminModule depends on
starterModule. The starter depends on the OAuth module.
The list is then folded left to right. Your files are last. On every key that overrides by key, yours is the version that survives.
Duplicates collapse by name
A module that appears twice in the resolved list is kept once, at its last position. Two modules that both depend on the starter do not merge the starter twice.
The key is the name string, not the object identity. Two different objects
that both call themselves blog collapse into one, and the later one survives.
Give every module a unique, stable name.
The config bucket
config holds one key per config/*.ts file. Each key merges on its own.
| Config key | Rule |
|---|---|
auth | Deep merge. Plugins concatenate and de-dup by plugin id, incoming first. Social providers spread. |
admin | Sidebar items append across modules. dashboard deep-merges. Everything else takes the later value. |
app | locale, access and context take the later value. hooks concatenate. |
| any other key | The later value replaces the whole object. |
Set sidebarMode: "replace" in your own config/admin.ts to keep only your
sidebar and drop the module contributions.
The auth rule is what makes session.user.role exist in an admin app. The
starter's config/auth.ts contributes Better Auth's admin() plugin, and your
own config/auth.ts merges on top without dropping it.
// After `questpie generate`, this compiles in handlers and access rules.
({ session }) => session?.user.role === "admin";Codegen plugins are collected separately
plugin never reaches the merge above. Codegen walks the same module tree in a
pre-pass, depth-first. It collects every plugin and de-dups by the plugin's
name. The first occurrence wins there, not the last.
Plugins you passed to runtimeConfig({ plugins }) are seen first. So a
config-level plugin beats a module plugin with the same name.
Two phases, one package
A module's entities merge at runtime. Its plugin runs at codegen. If the entities show up but the file conventions do not, check that the package ships both.
Adapters
An adapter is the delivery backend. QUESTPIE ships four, each on its own import path, and any class extending MailAdapter can be the fifth.
Publishing
Ship a module on npm the way QUESTPIE ships its own. You describe the package once with packageConfig, and codegen writes the static module object from the same file convention an app uses.