---
title: "systhema setup"
description: "Add, reconfigure or turn off database, email, maps, captcha, forms, redirects, AI, Cloudflare and locales."
requested_language: nl
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/nl/cli/setup
version: unreleased (main)
docs_index: https://docs.systhema.app/nl/llms.txt
---
> This page isn't translated yet. Showing English.


Add or reconfigure Payload features — **database**, **email**, Google **maps**, form **captcha**, **forms**, **redirects**, the **AI assistant**, **Cloudflare**, or **locales** — on an already-scaffolded project, without re-running `create` (which requires an empty target directory and can't touch an existing project). This is how you answer "I skipped email during `create`, how do I add it now?", "I want to switch from SQLite to Postgres", or "turn the Forms module off."

```bash
systhema setup                                    # state overview + interactive checkbox picker
systhema setup email                              # configure one feature interactively
systhema setup --email smtp --smtp-host "$SMTP_HOST" --yes   # feature implied by flags, no positional needed
systhema setup ai --dry-run                        # plan only, no changes
systhema setup forms --forms off                   # toggle a module off (confirms first)
systhema setup locales --locales en,hu --default-locale en --yes
```

`setup` reuses [`create`'s per-feature flags, env vars, and non-interactive credential resolution](https://docs.systhema.app/nl/cli/create.md#non-interactive-credential-resolution) verbatim — `--db`, `--db-url`, `--email`, `--email-from`, `--resend-key`, `--sendgrid-key`, `--smtp-*`, `--captcha`, `--recaptcha-*`, `--turnstile-*`, `--maps-key`, `--forms`, `--redirects`, `--ai`, `--ai-key`, `--ai-model`, `--ai-base-url`, `--cloudflare`, `--cloudflare-api-token`, `--cloudflare-api-key`, `--cloudflare-email`, `--cloudflare-zone-id`, `--locales`, `--default-locale`. See the [`create` flag table](https://docs.systhema.app/nl/cli/create.md) for what each one does.

## Choosing features

The features to touch are picked in priority order:

1. **Positional argument** — `systhema setup email` touches only the email feature.
2. **Implied by flags** — a feature-specific flag passed without a positional (e.g. `systhema setup --email smtp --yes`) is enough; `setup` infers the feature(s) to run from which flags were set.
3. **Interactive picker** — bare `systhema setup` prints every feature's current state, then opens a checkbox prompt to pick which ones to (re)configure.

A non-interactive run (`--yes`) that can't resolve a feature from either a positional or flags exits with an error asking for one.

## Options

<!-- generated:cli-flags setup -->

| Flag                             | Description                                                                                                         | Default |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------- |
| `--ai <provider>`                | AI single provider: none \| openai \| anthropic \| google \| groq \| openai-compatible ('none' disables the module) |         |
| `--ai-base-url <url>`            | AI endpoint base URL (env: SYSTHEMA_AI_BASE_URL)                                                                    |         |
| `--ai-key <key>`                 | AI provider API key (env: SYSTHEMA_AI_API_KEY)                                                                      |         |
| `--ai-model <model>`             | AI model id (env: SYSTHEMA_AI_MODEL)                                                                                |         |
| `--captcha <choice>`             | Form captcha: none \| recaptcha \| turnstile \| both                                                                |         |
| `--cloudflare <mode>`            | Cloudflare auth: token \| key \| none (env: SYSTHEMA_CLOUDFLARE_MODE)                                               |         |
| `--cloudflare-api-key <key>`     | Cloudflare Global API key (env: CLOUDFLARE_API_KEY)                                                                 |         |
| `--cloudflare-api-token <token>` | Cloudflare API token (env: CLOUDFLARE_API_TOKEN)                                                                    |         |
| `--cloudflare-email <email>`     | Cloudflare account email (env: CLOUDFLARE_EMAIL)                                                                    |         |
| `--cloudflare-zone-id <id>`      | Cloudflare zone ID (env: CLOUDFLARE_ZONE_ID)                                                                        |         |
| `--confirm-db-switch`            | Required with --yes to switch the database adapter                                                                  | `false` |
| `--confirm-disable`              | Required with --yes to disable a currently-enabled module                                                           | `false` |
| `--db <adapter>`                 | Database adapter: sqlite \| postgres                                                                                |         |
| `--db-url <uri>`                 | Database connection string (env: DATABASE_URI)                                                                      |         |
| `--default-locale <code>`        | Default locale (must be in --locales)                                                                               |         |
| `--dry-run`                      | Plan only, no changes                                                                                               | `false` |
| `--email <adapter>`              | Email adapter: resend \| sendgrid \| smtp \| none                                                                   |         |
| `--email-from <addr>`            | Default from address (env: SYSTHEMA_EMAIL_FROM)                                                                     |         |
| `--force`                        | Allow dirty tree + overwrite hand-edited modules + allow config line replaces                                       | `false` |
| `--forms <on\|off>`              | Forms module: on \| off                                                                                             |         |
| `--locales <codes\|none>`        | Locales: comma-separated BCP 47 codes (e.g. en,hu), or 'none' to disable                                            |         |
| `--maps-key <key>`               | Google Maps API key (env: NEXT_PUBLIC_GOOGLE_MAPS_API_KEY)                                                          |         |
| `--no-git-check`                 | Skip git repo/clean checks (CI mode)                                                                                | `true`  |
| `--no-install`                   | Skip dependency install after changes                                                                               | `true`  |
| `--recaptcha-secret-key <key>`   | reCAPTCHA secret key (env: RECAPTCHA_SECRET_KEY)                                                                    |         |
| `--recaptcha-site-key <key>`     | reCAPTCHA site key (env: NEXT_PUBLIC_RECAPTCHA_SITE_KEY)                                                            |         |
| `--redirects <on\|off>`          | Redirects module: on \| off                                                                                         |         |
| `--resend-key <key>`             | Resend API key (env: RESEND_API_KEY)                                                                                |         |
| `--sendgrid-key <key>`           | SendGrid API key (env: SENDGRID_API_KEY)                                                                            |         |
| `--smtp-host <host>`             | SMTP host (env: SMTP_HOST)                                                                                          |         |
| `--smtp-pass <pass>`             | SMTP password (env: SMTP_PASS)                                                                                      |         |
| `--smtp-port <port>`             | SMTP port (env: SMTP_PORT)                                                                                          |         |
| `--smtp-user <user>`             | SMTP user (env: SMTP_USER)                                                                                          |         |
| `--turnstile-secret-key <key>`   | Turnstile secret key (env: TURNSTILE_SECRET_KEY)                                                                    |         |
| `--turnstile-site-key <key>`     | Turnstile site key (env: NEXT_PUBLIC_TURNSTILE_SITE_KEY)                                                            |         |
| `-y, --yes`                      | Skip prompts                                                                                                        | `false` |

<!-- /generated -->

**Non-interactive re-runs never disable by omission.** Under `--yes`, a feature you don't give a flag for keeps whatever it is already set to — `systhema setup ai --yes` on a project with AI configured is a no-op, not a switch-off. Turning something off is always explicit (`--ai none`, `--forms off`, `--locales none`), and still needs `--confirm-disable`.

**Safety:** `setup` only runs against **Payload** projects (the `next`/`html` templates have no server-side integrations to configure) and, like [`upgrade`](https://docs.systhema.app/nl/cli/upgrade.md), preflights on a clean git tree unless `--force` / `--no-git-check`.

## Divergence handling

Each feature's generated module (`src/payload/database.ts`, `src/payload/email.ts`, `src/payload/forms.ts`, `src/payload/redirects.ts`, `src/payload/ai.ts`, `src/payload/cloudflare.ts`) is compared, whitespace-normalized, against every render the current CLI's generators can produce (for a toggle that means **both** the on and the off render, so flipping a module is a pristine overwrite):

- **Pristine** (matches some generated render, untouched since scaffolding or a previous `setup`) — overwritten silently.
- **Hand-edited** (doesn't match any known render) — prints a diff and asks for confirmation before overwriting. A module scaffolded by an older CLI version won't match a current render either, so it's treated the same way — you'll see a diff and a confirm, never a silent overwrite. Under `--yes`, a diverged module is **skipped** — and the command exits with status `1` — unless `--force` is also passed. Declining the interactive overwrite confirmation has the same effect — the module is left untouched and the command exits with status `1`.

## `.env` handling

`setup` edits `.env` in place. Detection reads `.env`, `.env.development`, `.env.production`, and `.env.local` (later files override earlier ones) so a value set anywhere is recognized, but every write lands in `.env`:

- Existing values are never blanked by an empty answer — leave a prompt empty to keep whatever's already set.
- Switching away from a provider (e.g. Resend → SMTP, or a keyed AI provider → none) comments out the old provider's keys instead of deleting them, preserving the values. Switching back later without supplying a new value restores them uncommented.
- `PAYLOAD_SECRET` and `SYSTHEMA_API_SECRET` are never regenerated.
- A project with no `.env` at all gets one created fresh, seeded from whatever `setup` can detect (installed adapters, existing env values) plus this run's choices.

## Database switch

Switching the database adapter (e.g. SQLite → Postgres) swaps the dependency and rewrites `src/payload/database.ts`, but **your existing data is not migrated**. `setup` prints a loud warning and asks for confirmation before proceeding; under `--yes` you must also pass `--confirm-db-switch`, or the switch is skipped and the command exits with status `1`. Declining the interactive confirmation has the same effect — the switch is skipped and the command exits with status `1`.

## Module toggles

**Forms** and **Redirects** are plain on/off modules rather than credential-carrying integrations. They own no `.env` variables — a module registers collections, and schema-shaping state belongs in checked-in source, not in per-environment variables — so the toggle state lives entirely in a generated file:

| Module      | File                       | Export     | What it controls                                                                                           |
| ----------- | -------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------- |
| `forms`     | `src/payload/forms.ts`     | `forms`    | The form builder, the Forms + Form Submissions collections, and the form block in editors.                 |
| `redirects` | `src/payload/redirects.ts` | `redirect` | The Redirects collection (including [wildcard rules](https://docs.systhema.app/nl/payload/content/redirects.md#wildcard-redirects)). |

`payload.config.ts` wires each one by shorthand (`forms,` / `redirect,`), the same way the database, email, AI, analytics and Cloudflare modules are wired, so `systhema setup forms --forms off` is a one-file rewrite.

```bash
systhema setup forms --forms off       # confirms, then writes the disabled render
systhema setup redirects --redirects on
```

Two things are worth knowing:

- **The Forms toggle never touches dependencies** — `@payloadcms/plugin-form-builder` is a regular dependency of `@systhemaui/payload`. **The Redirects toggle only ever adds one**: `@payloadcms/plugin-redirects` is a required peer of `@systhemaui/payload`, so enabling on a project that somehow lacks it adds it to `devDependencies`, and disabling never removes it (an "unused" install is the correct steady state — removing it would produce an unmet-peer warning).
- **`--ai none` is the same kind of toggle** — it disables the AI assistant module, commenting out the `SYSTHEMA_AI_*` variables rather than deleting them. It carries no disable confirmation: no user-authored collection disappears (the hidden AI usage log simply stops being registered, and its rows persist).

### The disable guard

Turning a module **off never touches your data** — the plugin just stops registering the collections, and the rows stay in the database. The risk is indirect (a later dev-mode schema push may offer to drop the now-orphaned tables — decline unless you mean it) and re-enabling restores access to everything.

Because it's still a visible, content-affecting change, `setup` prints the data-impact warnings and asks for confirmation, defaulting to **No**. Under `--yes` you must pass `--confirm-disable`; otherwise the disable is skipped and the command exits with status `1` — the exact mirror of `--confirm-db-switch`. If the module file was also hand-edited, you'll get both prompts: the diverged-file diff confirm and the disable confirm.

```bash
systhema setup forms --forms off --yes --confirm-disable
```

## Adopting missing modules

If a project predates a given feature (no `src/payload/database.ts` / `email.ts` / `forms.ts` / `redirects.ts` / `ai.ts` / `cloudflare.ts` yet), `setup` creates the missing module and tries to splice its wiring into `payload.config.ts` — adding the import plus the wiring itself. For `db`/`email` this means replacing a single, comma-terminated wiring line; `forms`, `redirect`, `ai` and `cloudflare` are inserted as a new property into the plugin-options object, or — when the key is already there as a **single-line** value (`redirect: true,` in pre-toggle projects) — replaced with the shorthand through the usual replace confirmation. Anything ambiguous — an unusual shape, an existing conflicting member, a multi-line expression — is left untouched, and `setup` prints the exact lines to add by hand instead.

The pre-toggle template's inline `forms: { … }` object is exactly such a multi-line expression, so on an older project `setup forms` writes the module but prints the wire step as a manual edit. The smooth path is [`systhema upgrade`](https://docs.systhema.app/nl/cli/upgrade.md), whose `extract-forms-redirects-modules` codemod extracts both members into modules for you (it skips a customized `forms` object rather than guessing).

## Google Maps

`systhema setup maps` sets `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY`. There's no "disable" choice for maps — to turn it off, comment out `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY` in `.env` yourself.

## Locales

`systhema setup locales` is the multilingual entry point. Unlike every other feature it writes neither `src/payload/*` nor `.env`: [`systhema.config.ts`](https://docs.systhema.app/nl/nextjs/locales.md) is the canonical locale source (`payload.config.ts` already projects it via `locales: systhemaConfig.locales`), so the connector edits that one file and then runs the project's own locale migration.

```bash
systhema setup locales                                              # interactive
systhema setup locales --locales en,hu --default-locale en --yes    # enable / reconfigure*
systhema setup locales --locales none --yes --confirm-disable       # back to one language
```

What it does, in order:

1. **Edits `systhema.config.ts`** — adds the `defineSysthemaLocales({ supported, default })` block, the `@systhemaui/core` import, and the `locales,` member on the config object (or removes all three when disabling). The plan preview shows it as `~ systhema.config.ts`. Anything it doesn't recognize — a `systhema.config.js`, a multi-line `locales:` member, a missing `SysthemaConfig` anchor — is left untouched and printed as a manual step.

   It also **declines to rewrite a contract that carries settings it doesn't collect**. A definition holding `routing.domains`, `rtl`, `subfolder`, `missing` or `fallback` is left exactly as it is, and the block to paste is printed instead — replacing it wholesale would silently delete a production domain map, flip an RTL locale back to LTR, or reset the missing-translation policy. In-place reconfiguration therefore applies to a contract holding only `supported` and `default`; edit a richer one by hand.

2. **Runs `<pm> exec systhema-core sync --migrate-locales`** after the dependency install, shown in the plan preview as a `→` line (and skipped under `--dry-run`). That's the project's own `@systhemaui/core` doing the structural work: wrapping `next.config`, adding `locales` to `payload.config.ts`, and converting the site shell / route boundary.

   If it fails, the command **prints the migration's own output and exits non-zero**. The config edit stays on disk — it is exactly the input a re-run reads — but the project is half-migrated until the migration completes, so the run must not look successful to a script. (The most common cause is a customized `app/(site)/template.tsx`, which the migration refuses to convert.)

Enabling locales also makes publication status per-locale: `_status` becomes a localized field on Pages, Posts and Components, so a locale can be published while another stays a draft ([Per-locale publishing](https://docs.systhema.app/nl/payload/localization/per-locale-publishing.md), opt out with `localization: { localizeStatus: false }`). A project with existing content therefore has to include the `_status` move in the reviewed migration plan it writes for the rest of the schema ([Localization migration](https://docs.systhema.app/nl/payload/localization/migrating-existing-site.md)). `systhema migrate migrate-localize-status` performs that half.

The wizard collects **only** the supported codes and the default locale. RTL, per-locale domains, subfolders, `missing` behavior and frontend `messages` stay hand-edited in `systhema.config.ts` — see [Frontend locales](https://docs.systhema.app/nl/nextjs/locales.md). \*Because of that split, reconfiguration edits a contract holding just those two members; one carrying hand-authored settings is left alone with printed instructions (step 1).

> [!WARNING]
> **Existing content:** enabling locales changes the content schema (localized columns). A project that already holds content needs the reviewed plan in [Localization migration](https://docs.systhema.app/nl/payload/localization/migrating-existing-site.md) — never point an inferred schema rewrite at a real database. Disabling is guarded like the other toggles (`--confirm-disable` under `--yes`): localized columns are **not** dropped, but non-default-locale translations stop being served, and the migration refuses to discard a customized site shell.
