# Building a plugin (/docs/guides/build-a-plugin)

---
title: Building a plugin
description: A plugin teaches the generator a directory of your own. Declare it once, and every app that installs your package picks up that convention, with no central registry to edit.
kind: guide
package: questpie
---

You want a directory of your own. Drop a file into `webhooks/`, and have the
generator collect it the way it collects a collection. This page builds the
plugin that does that, from an empty file to a working result.

## Check that you need one

Most extension is not a plugin. A plugin adds a new **place** for the generator
to look. If you want a new **value** where it already looks, write the value.

| You want                                | Write this instead                                              |
| --------------------------------------- | --------------------------------------------------------------- |
| A new `f.*` type                        | [A field type](/docs/guides/build-a-plugin/field-types)         |
| A method on `collection()`              | [A builder method](/docs/guides/build-a-plugin/builder-methods) |
| Another queue, search or realtime layer | An adapter. See [Infrastructure](/docs/infrastructure)          |
| A unit for the block editor             | A file in `blocks/`. See [Blocks](/docs/schema/blocks)          |
| A directory of your own                 | Keep reading                                                    |

## What you are building

Four files. The first two are yours. The last two are what someone using your
plugin writes.

```
src/questpie/server/
  webhook.ts           # the factory your files call
  plugin.ts            # the plugin: build time here, also loaded at runtime once shipped in a module
  questpie.config.ts   # registers it
  webhooks/stripe.ts   # the new convention, in use
```

## 1. Write the factory

The generator does not care what your factory returns. It needs a name to scan
for.

```ts title="src/questpie/server/webhook.ts"
export function webhook(
	name: string,
	config: { path: string; handler: (payload: unknown) => Promise<void> },
) {
	return { name, ...config };
}
```

## 2. Declare the convention

A plugin is one object. It has a name and target contributions. The `server`
target writes `index.ts`, so `root` and `outputFile` must match the core plugin.

```ts title="src/questpie/server/plugin.ts"
import type { CodegenPlugin } from "questpie/codegen";

export function webhooksPlugin(): CodegenPlugin {
	return {
		name: "acme-webhooks",
		targets: {
			server: {
				root: ".",
				outputFile: "index.ts",
				categories: {
					webhooks: {
						dirs: ["webhooks"],
						prefix: "webhook",
						factoryFunctions: ["webhook"],
						registryKey: true,
					},
				},
			},
		},
	};
}
```

`dirs` is what to scan. `prefix` names the variables in the generated file.
`factoryFunctions` is the switch that matters. It says a `webhook()` call marks
an entity. One file can then hold several, and a helper file holds none.

## 3. Register it

```ts title="src/questpie/server/questpie.config.ts"
import { runtimeConfig } from "questpie/app";

import { webhooksPlugin } from "./plugin";

export default runtimeConfig({
	db: { url: process.env.DATABASE_URL! },
	plugins: [webhooksPlugin()],
});
```

<Callout type="warn" title="`plugins` is not `modules.ts`">
	`plugins` takes codegen plugins. `modules.ts` takes package dependencies. A
	module can carry its own plugin, so most apps never write `plugins` at all.
	The core plugin is prepended on every run and you never register it.
</Callout>

## 4. Use the convention

```ts title="src/questpie/server/webhooks/stripe.ts"
import { webhook } from "../webhook";

export default webhook("stripe", {
	path: "/hooks/stripe",
	async handler(payload) {},
});
```

## 5. Generate and read it back

```bash
questpie generate
```

```ts
import { app, type AppWebhooks } from "#questpie";

const webhooks = app.state?.webhooks as AppWebhooks;
webhooks.stripe.path; // "/hooks/stripe"
```

That is the working result. The key is `stripe` because the first string
argument wins over the file name, with kebab-case turned to camelCase.

A built-in category such as `collections` gets a home of its own on the app.
Yours lands under `app.state`, typed `Record<string, unknown>`. So you cast, the
way `@questpie/mcp` does. Codegen names the type after your category.

If your factory returns a builder with a `.build()` method, the app calls it for
you and stores the built value.

## Add a scaffold

Add a template and your users get a `questpie add` command for it.

```ts title="src/questpie/server/plugin.ts"
scaffolds: {
	webhook: {
		dir: "webhooks",
		description: "Webhook handler",
		template: ({ kebab, camel }) =>
			`import { webhook } from "../webhook";\n\nexport const ${camel} = webhook("${kebab}", {\n\tpath: "/hooks/${kebab}",\n\tasync handler(payload) {},\n});\n`,
	},
},
```

`questpie add webhook stripe` now writes the file. `questpie add --list` shows
it beside the built-in types. An existing file is skipped with a warning.

## Claim a config file

Your plugin probably needs settings. Claim a file instead of a `runtimeConfig`
key. Add a discover pattern, then type the key so users get autocomplete.

```ts title="src/questpie/server/plugin.ts"
discover: {
	webhooksConfig: { pattern: "config/webhooks.ts", configKey: "webhooks" },
},
```

```ts title="src/questpie/server/plugin.ts, at the top level"
declare module "questpie" {
	interface AppStateConfig {
		webhooks?: { secret: string };
	}
}
```

A `config/webhooks.ts` now lands on `app.state.config.webhooks`. Pick a key no
other plugin will claim, because nothing checks. Two modules that set the same
key are last-wins on the whole object. Only `app`, `auth` and `admin` merge
below the key.

## Ship it

Move `plugin.ts` into a module directory in your package. Codegen puts it on
that module's `plugin` key. The consuming app picks it up from `modules.ts` with
no extra config. That is how `@questpie/admin` and `@questpie/mcp` install
themselves. [Publishing](/docs/code/modules/publishing) has the layout.

The `plugin` key holds a live value, not a specifier. On this path the plugin
file and everything it imports load in the server process of every app that
lists your module, so keep those imports light. Giving the plugin its own entry
point, like `@acme/webhooks/plugin`, helps only on the other path, where an app
registers it by hand in `questpie.config.ts`.

## Where each topic lives

| Topic                                   | Page                                        |
| --------------------------------------- | ------------------------------------------- |
| How a file becomes a key                | [Discovery](/docs/code/codegen/discovery)   |
| Shipping a plugin on npm                | [Publishing](/docs/code/modules/publishing) |
| Config files from the app author's side | [Configuration](/docs/ship/configuration)   |

## Next

**[Plugins](/docs/code/codegen/plugins)** is the same contract as a reference.
Every option on a category, a discover pattern and a registry, with its default.
