# Plugin config files (/docs/ship/configuration/plugins)

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

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.

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

```ts
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.

```ts title="src/questpie/server/config/openapi.ts"
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.

```ts title="src/plugin.ts"
registries: {
	singletonFactories: {
		myPluginConfig: {
			configType: "MyPluginConfig",
			imports: [{ name: "MyPluginConfig", from: "my-plugin" }],
		},
	},
},
```

```ts title="src/questpie/server/config/admin.ts"
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.

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

## Shipping modules in a package

A package that ships modules describes itself once, and `questpie generate`
builds a static module per subdirectory.

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

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

## 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](/docs/ship/configuration/runtime).

## Next

**[Codegen](/docs/code/codegen)** covers the rest of the plugin surface,
categories, registries and emitted output.
