Plugin config files
Every file in config/ becomes one key on the app's config bucket. A plugin claims its own by declaring a single discover pattern, with no change to framework code.
You are shipping a plugin and it needs settings. Where does the user write them, and how do they reach you at runtime?
One file, one key
config/app.ts and config/auth.ts are not special cases. Every file in
config/ follows one rule. The plugin that discovers it names the key, and the
whole file becomes the value. Every shipped plugin names the key after the file.
| File | Key | Comes from |
|---|---|---|
config/app.ts | config.app | core |
config/auth.ts | config.auth | core |
config/admin.ts | config.admin | @questpie/admin |
config/openapi.ts | config.openapi | @questpie/openapi |
config/mcp.ts | config.mcp | @questpie/mcp |
config/workflows.ts | config.workflows | @questpie/workflows |
At runtime the merged bucket sits on app.state.config. Every plugin reads its
own key from there.
Claiming one
Add a discover pattern to your codegen plugin. configKey is what turns a
discovered file into a config entry instead of a plain single.
import type { CodegenPlugin } from "questpie";
export function myPlugin(): CodegenPlugin {
return {
name: "my-plugin",
targets: {
server: {
root: ".",
outputFile: "index.ts",
discover: {
myPluginConfig: {
pattern: "config/my-plugin.ts",
configKey: "myPlugin",
},
},
},
},
};
}That is the whole registration. Codegen now folds config/my-plugin.ts into
config.myPlugin, and a module contributing the same key is merged in too.
Typing the key
AppStateConfig is the interface the bucket is typed against. Augment it so
your key is known.
declare module "questpie" {
interface AppStateConfig {
myPlugin?: MyPluginConfig;
}
}At the core the interface is just { app?, auth? }. Everything else is
augmentation. The resolved type carries an index signature too. So an
un-augmented key still works. It is just untyped.
Giving users a factory
Users need a typed function to wrap their config in. There are two ways.
Export one from your package, the way @questpie/openapi does.
import { openApiConfig } from "@questpie/openapi";
export default openApiConfig({
info: { title: "My API", version: "1.0.0" },
});Or declare a singleton factory beside your discover pattern and let codegen emit
one into #questpie/factories. That is where adminConfig comes from.
registries: {
singletonFactories: {
myPluginConfig: {
configType: "MyPluginConfig",
imports: [{ name: "MyPluginConfig", from: "my-plugin" }],
},
},
},import { adminConfig } from "#questpie/factories";
export default adminConfig({
branding: { name: "My site" },
});Both are identity functions. They exist so TypeScript infers the shape.
How the merge runs
Codegen collects one value per key per module, then folds them.
A key with no declared strategy takes the last value, whole. app is merged one
sub-key at a time, so hooks concatenate while locale, access and context
take the last file. auth and admin each have a deep merge of their own.
Modules are folded before your project, so your own config/ files land last
and win.
`configKey` and `registryKey` are different jobs
configKey emits the whole file as one entry in the config bucket.
registryKey adds the file to a type registry instead. A config file wants
the first.
Shipping modules in a package
A package that ships modules describes itself once, and questpie generate
builds a static module per subdirectory.
import { packageConfig } from "questpie/cli";
import { myPlugin } from "./src/server/plugin.js";
export default packageConfig({
modulesDir: "src/server/modules",
modulePrefix: "questpie",
plugins: [myPlugin()],
});questpie generate scans modulesDir, names each module
<modulePrefix>-<dirName>, and writes a .generated/module.ts into each one.
modulePrefix defaults to questpie.
This config is development-only. It is not published. Only the generated
module.ts files ship. That is why a consumer adds your module to modules.ts
and gets your file conventions with no further wiring.
Importing `questpie/cli` must not run a command
Command parsing is guarded by import.meta.main, so importing the module is
side-effect free. Package config files import it for packageConfig, and a
stray second instance running a command would corrupt .generated/.
Runtime keys are a separate channel
A config/<name>.ts file is composite config the user authors. Some plugins need
an adapter or a connection instead. That takes a key on runtimeConfig(), typed
by augmenting RuntimeConfigExtensions. See
Runtime options.
Next
Codegen covers the rest of the plugin surface, categories, registries and emitted output.
App config
config/app.ts holds four app-wide settings, locales, fallback access, global hooks and the per-request context resolver. Each one merges differently when a module already set it.
Options
Every option env() takes, the twelve variables QUESTPIE merges under yours, and the flag that lets a build run with no environment at all.