---
title: "withSysthema()"
description: "Wrap buildConfig(), import paths, 'use client' imports, and discovering Systhema from another plugin."
requested_language: cs
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/cs/next/payload/with-systhema
version: unreleased (main)
docs_index: https://docs.systhema.app/cs/next/llms.txt
---
> This page isn't translated yet. Showing English.


Wrap your `buildConfig()` call with `withSysthema()`. Systhema deep-merges its defaults under your overrides.

```ts title="src/payload.config.ts"
import { sqliteAdapter } from '@payloadcms/db-sqlite'
import {
  withSysthema,
  type SysthemaPayloadPluginOptions,
} from '@systhemaui/payload'
import { buildConfig } from 'payload'

const userSysthemaConfig: SysthemaPayloadPluginOptions = {
  // see "Plugin options" below
}

export default buildConfig(
  withSysthema(
    {
      admin: {
        dateFormat: 'yyyy-MM-dd HH:mm:ss',
      },
      secret: process.env.PAYLOAD_SECRET || '',
      db: sqliteAdapter({
        client: { url: process.env.DATABASE_URI || '' },
      }),
    },
    userSysthemaConfig,
  ),
)
```

You can let Systhema generate this file for you with:

```bash
pnpm systhema-core payload create-config
```

For every entry point and what it exports, see [Payload import paths](https://docs.systhema.app/cs/next/reference/payload-imports.md).

## Imports in `'use client'` files

Everything listed on [Payload import paths](https://docs.systhema.app/cs/next/reference/payload-imports.md) is **server** surface. A `'use client'` file — yours or one Systhema renders — must be pickier, because a single runtime import can drag a server-only module graph into the browser bundle. Three rules:

| In a `'use client'` file, import…                                                                                                    | From                              | Never from                                          |
| ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- | --------------------------------------------------- |
| Systhema config, resolved token values                                                                                               | `@systhemaui/core/client`         | `@systhemaui/core`                                  |
| Pure helpers (`toKebabCase`, `toUrlCase`, `deepMerge`, …)                                                                            | `@systhemaui/core/utils`          | `@systhemaui/core`                                  |
| `RichText`, `RenderForm`, `PostsListClient`, `ArchiveFilterProvider`, `Post*`                                                        | `@systhemaui/payload/next/client` | `@systhemaui/payload` or `@systhemaui/payload/next` |
| `registerSysthemaClientBlocks`, `registerSysthemaClientTextStates`, types `CustomBlock` / `CustomBlockConverter` / `CustomTextState` | `@systhemaui/payload/next/client` | `@systhemaui/payload`                               |
| `resolveIconHtml`, `resolveIconForRender`                                                                                            | `@systhemaui/payload/next/client` | `@systhemaui/payload/fields`                        |
| `useSysthemaIconResolution`                                                                                                          | `@systhemaui/payload/next/client` | —                                                   |

- `@systhemaui/core`'s main entry statically reaches the design-token dataset (14 DTCG JSON files, >1 MB in a real project). JSON modules are whole-module, so one value import ships all of it. See [`@systhemaui/core/client`](https://docs.systhema.app/cs/next/concepts/client-and-server.md).
- Resolving an icon is client-safe; **defining** the picker is not. `resolveIconHtml` reads an in-memory cache the server fills, so it works in the browser. `iconPickerField` from `@systhemaui/payload/fields` builds an admin schema. A custom block that keeps its `fields` and its `converter` in one module puts both in the browser bundle the moment you register the converter for client-mode live preview — split the converter into its own module.
- That in-memory cache is **empty in the browser**, so Material Symbols and Apple emoji picks resolve to nothing there. Client-mode live preview fetches the handful of ids a page uses from `GET <routes.api>/systhema/icons/resolve` and re-renders; a hand-rolled browser-rendered tree calls `useSysthemaIconResolution(apiRoute)` itself. See [Icons → Rendering icons in the browser](https://docs.systhema.app/cs/next/payload/editor/icons.md#rendering-icons-in-the-browser).
- The `@systhemaui/payload` and `@systhemaui/payload/next` barrels reach the Payload **admin** graph — `payload`, `@payloadcms/ui`, `@payloadcms/next` and their translation catalogue for every supported language, plus the icon-pack `fs` runtime. `@systhemaui/payload/next/client` is barrel-free and exists precisely for this.

Systhema's own client-reachable modules follow these rules, and a package test walks the import graph — over `src/` **and** the built `dist/` — to keep it that way.

The same guard covers two things that are client-safe but **optional**, so a static import would make every visitor pay for a feature their site may not use:

- **Country / state option lists.** The `country` and `state` form fields render ~10 KB of fixed names. They are resolved on the server and reach the browser as a serialized prop, so only a page that actually renders one of those fields carries the list — in its flight payload, never in a JS chunk. (Both field types are off by default; enable them via `forms.fields`.)
- **The built-in Posts page/post templates.** Their client-side live-preview views load behind `import(…)`, so the Posts presentation graph stays out of the bundle on sites with the Posts module off, and out of the published page everywhere.

`RenderForm` is a **Server Component** for the first of those reasons — imported from `@systhemaui/payload/next` it resolves the option lists on the server and hands them to the client form, which is where you want it. The client boundary is inside it.

It is also exported from `@systhemaui/payload/next/client`, because a page whose template renders a form otherwise could not have a `clientView` at all — it would be stuck on server-mode live preview while the rest of the site used client mode. Its graph is client-safe in full. The one thing that does not survive the crossing is the option-list read: in the browser the server-side store is empty by construction, so a `country` or `state` field renders an empty select. Resolve the lists in a server module and thread them back:

```ts
// The template's resolveData hook — a SERVER module.
import { resolveFormFieldOptions } from '@systhemaui/payload/next'

export const resolveData = async ({ data }: PageTemplateDataContext) => ({
  fieldOptions: resolveFormFieldOptions(data.form),
})
```

```tsx
'use client'
import { RenderForm } from '@systhemaui/payload/next/client'

export default function EntryClientView({ data, aux }: LivePreviewClientView) {
  return <RenderForm form={data.form} fieldOptions={aux.fieldOptions} />
}
```

Omit `fieldOptions` on the server — the store is read directly — and omit it in a client view whose forms use neither field type (both are off by default).

> [!TIP]
> **Admin translations.** Payload bundles the admin UI's translations for every language it supports. That weight lives in the `/admin` route, not on your public pages, but if you never switch the admin language you can drop it with `i18n: { supportedLanguages: { en } }` (from `@payloadcms/translations/languages/en`) in your Payload config — `withSysthema()` doesn't set `i18n`, so your value is used as-is.

## From another plugin

Systhema's plugin registers itself under the slug `systhema`, so any other plugin in the same config can read the options Systhema was configured with instead of parsing `systhema.config.ts` a second time.

```ts
import type { Plugin } from 'payload'

const myPlugin: Plugin = (config) => {
  const systhema = (config.plugins ?? []).find((plugin) => plugin.slug === 'systhema')
  const postsEnabled = Boolean(systhema?.options?.posts)
  // ...
  return config
}
```

Payload builds that lookup for you when the plugin is authored with `definePlugin`, which hands your plugin a slug-keyed `plugins` map alongside `config`. `@systhemaui/payload` augments Payload's `RegisteredPlugins` interface, so `plugins.systhema` is typed and its options need no cast:

```ts
import { definePlugin } from 'payload'

export const myPlugin = definePlugin({
  slug: 'my-plugin',
  plugin: ({ config, plugins }) => {
    const postsEnabled = Boolean(plugins.systhema?.options.posts)
    return config
  },
})
```

Two things to know. The options you read back are the normalized ones `withSysthema()` resolved, not the literal object you passed it. And Systhema leaves `order` unset, so it keeps its array position: `withSysthema()` registers it first, ahead of the plugins it configures.

Payload marks `slug`, `order` and `options` experimental, so treat this API as a moving target across Payload minors.
