# Configuration (/docs/ship/configuration)

---
title: Configuration
description: A QUESTPIE app is configured by a handful of files, each owning one concern. Which file owns which setting, what resolves from the environment on its own, and what a setting you never write falls back to.
kind: guide
package: questpie
---

You have a database URL, a signing secret, an email adapter and a list of
locales. Each one goes somewhere different. Here is the map.

## The files

They all sit under `src/questpie/server/`. Codegen reads them and writes
`.generated/`, which is what actually boots.

| File                 | Owns                                                       | Factory             |
| -------------------- | ---------------------------------------------------------- | ------------------- |
| `questpie.config.ts` | Database, adapters, secret, storage                        | `runtimeConfig()`   |
| `config/app.ts`      | Locales, default access, global hooks, per-request context | `appConfig()`       |
| `config/auth.ts`     | Better Auth options                                        | `authConfig()`      |
| `config/<name>.ts`   | One plugin's config, such as `config/admin.ts`             | the plugin's own    |
| `modules.ts`         | Pre-built modules you depend on                            | none, a plain array |
| `env.ts`             | Env var schema and validation                              | `env()`             |

Only `questpie.config.ts` is required. A scaffolded project ships that,
`modules.ts`, and two or three files under `config/`. It ships no
`config/app.ts` at all.

## The one you must write

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

import env from "./env"; // declared and validated in env.ts

export default runtimeConfig({
	app: { url: env.APP_URL },
	db: { url: env.DATABASE_URL },
	email: { adapter: new ConsoleAdapter() },
});
```

Then build the app surface.

```bash
questpie generate
```

`runtimeConfig()` holds infrastructure and nothing else. Collections, globals,
routes and jobs come from their own directories. There is no key here for them.

<Callout type="warn" title="`email.adapter` is not optional">
	The mailer starts with every app. Leave `email` out and startup throws
	`QUESTPIE: 'email.adapter' is required` before the first request lands. Use
	`ConsoleAdapter` in development.
</Callout>

## What resolves without you

Leave out `app`, `db`, `secret` or `storage` and `runtimeConfig()` reads the
environment. Your value wins, then the `QUESTPIE_*` name, then the standard one.

| Field     | QUESTPIE Cloud       | Standard             | If neither              |
| --------- | -------------------- | -------------------- | ----------------------- |
| `app.url` | `QUESTPIE_APP_URL`   | `APP_URL`            | `http://localhost:3000` |
| `db`      | `QUESTPIE_DB`        | `DATABASE_URL`       | throws                  |
| `secret`  | `QUESTPIE_SECRET`    | `BETTER_AUTH_SECRET` | `undefined`             |
| `storage` | `QUESTPIE_STORAGE_*` | none                 | local `./uploads`       |

`db` is the one with no default. If none of the three has it, `runtimeConfig()`
throws where you call it. On QUESTPIE Cloud the file collapses to what nothing
can guess.

```ts
export default runtimeConfig({
	email: { adapter: new ConsoleAdapter() },
});
```

## The rest of the slots

Every other key picks an adapter or flips a switch. All of them are optional.

| Key                       | Picks                                             |
| ------------------------- | ------------------------------------------------- |
| `queue`                   | The job backend. Required once you declare a job. |
| `search`                  | The index. Postgres full-text when you skip it.   |
| `kv`                      | The key-value store. In-memory when you skip it.  |
| `realtime`                | The live transport. Always on. This tunes it.     |
| `storage`                 | Where uploaded bytes land.                        |
| `logger`, `observability` | Log format, traces and metrics.                   |
| `executor`                | Sandboxed code execution. Off unless you set it.  |
| `autoMigrate`, `autoSeed` | Whether boot runs migrations and seeds.           |

[Runtime options](/docs/ship/configuration/runtime) has the full table, the four
shapes `db` accepts, and the storage keys.

## `config/app.ts`

One file, four app-wide concerns. Every key is optional and the file itself is
optional.

| Key       | Decides                                                    |
| --------- | ---------------------------------------------------------- |
| `locale`  | Which content locales exist and which one is the default.  |
| `access`  | The rule a collection falls back to when it declares none. |
| `hooks`   | Callbacks that run across every collection or global.      |
| `context` | What each request carries, resolved once, before anything. |

`context` is the one worth learning first. It runs once per HTTP request and its
return travels flat into every access rule, hook, route and `getContext()` call.

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

export default appConfig({
	access: { read: true },
	context: async ({ request }) => ({
		tenantId: request.headers.get("x-tenant-id"),
	}),
});
```

Calls inside the resolver run in system mode, so access rules are bypassed. The
resolver is the trusted derivation step.

<Callout type="warn" title="`access` replaces, it does not merge">
	Set the key and a module's whole map goes. Write `read: true` alone and
	`create`, `update` and `delete` fall back to "require a session", not to what
	the module wanted.
</Callout>

[App config](/docs/ship/configuration/app) covers all four keys.

## `config/auth.ts`

The whole file is Better Auth options. Anything Better Auth accepts goes here.

```ts title="src/questpie/server/config/auth.ts"
import { admin, bearer } from "better-auth/plugins";
import { authConfig } from "questpie/app";

export default authConfig({
	plugins: [admin(), bearer()],
	emailAndPassword: { enabled: true, requireEmailVerification: false },
});
```

`baseURL` comes from `app.url` and `secret` from `runtimeConfig()`, so you do not
repeat them. Setting either here still wins, because your options are spread
last.

Your session type is built from the whole module tree's auth config intersected
with this file. A module that adds a Better Auth plugin widens `AppSessionUser`
for you. If a field is missing, run `questpie generate` before you cast.

## `modules.ts`

A plain array of the pre-built modules you depend on.

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

Codegen reads this file first, in its own pass, so each module's file
conventions are registered before discovery runs. The tree is walked
dependencies first, and two modules with the same name collapse to the last one.
See [Modules](/docs/code/modules).

## Plugins bring their own file

`config/app.ts` and `config/auth.ts` are not special. Each `config/<name>.ts`
becomes `config.<name>`, and a plugin claims one with a single discover pattern.
That is how `@questpie/admin` gets `config/admin.ts`.
[Plugin config files](/docs/ship/configuration/plugins) shows how to claim one.

## Where each topic lives

| Topic                                          | Page                                                    |
| ---------------------------------------------- | ------------------------------------------------------- |
| Every `runtimeConfig()` key, `db`, `storage`   | [Runtime options](/docs/ship/configuration/runtime)     |
| Locales, default access, global hooks, context | [App config](/docs/ship/configuration/app)              |
| Claiming a `config/<name>.ts` from a plugin    | [Plugin config files](/docs/ship/configuration/plugins) |
| Declaring and validating env vars              | [Environment](/docs/ship/environment)                   |
| What the generator reads and writes            | [Codegen](/docs/code/codegen)                           |

## Next

**[Environment](/docs/ship/environment)** is where `env.APP_URL` and
`env.DATABASE_URL` in the example above come from.
