---
title: "Project structure"
description: "The three-tier layout every Systhema project shares and where your own code belongs."
requested_language: fr
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/fr/next/getting-started/project-structure
version: unreleased (main)
docs_index: https://docs.systhema.app/fr/next/llms.txt
---
> This page isn't translated yet. Showing English.


Use these file-ownership rules to decide where to put your code. Payload projects have managed routes and starter scaffolds; HTML and Next.js projects without Payload do not have the managed CMS route group.

## The three tiers

Every file in `src/` falls into exactly one bucket:

1. **Framework-managed**: refreshed during `systhema upgrade`, with local edits preserved for review beside a `.new` file. Carries a `DO NOT MODIFY` banner. Treat as part of `@systhemaui/*`; do not edit.
2. **Starter scaffolds**, written once at scaffold time, preserved on every subsequent upgrade. Yours to customize (logo, fonts, providers, metadata, 404 design). Carries a "Starter file scaffolded by Systhema" banner.
3. **Your territory**, entirely yours. Custom blocks, components, lib utilities, access functions, custom routes, anything not produced by the framework.

The `(systhema)/` route group and `src/proxy.ts` are framework-managed. The `(payload)/` route group is Payload's own: `create-app-files` writes it once when it is missing and upgrades never rewrite it; edit only its `custom.scss`, and let `payload generate:importmap` rewrite `admin/importMap.js`. `src/app/layout.tsx`, `src/app/(site)/template.tsx`, `src/app/(site)/globals.css`, `src/app/(site)/not-found.tsx`, and `src/app/robots.txt/route.ts` are starter scaffolds. Everything else, `src/blocks/`, `src/components/`, `src/lib/`, `src/access/`, custom routes, is yours.

| Tier              | Behaviour on upgrade                  | Examples                                                                                                                                              |
| ----------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| Framework-managed | Refreshed; local edits produce `.new` | `src/proxy.ts`, `src/app/(site)/(systhema)/[[...segments]]/page.tsx`, `(systhema)/(sitemaps)/sitemap.xml/route.ts`, `(systhema)/sys/[route]/route.ts` |
| Starter scaffolds | Written once, never overwritten       | `src/app/layout.tsx`, `src/app/robots.txt/route.ts`, `src/app/(site)/template.tsx`, `src/app/(site)/globals.css`, `src/app/(site)/not-found.tsx`      |
| Your territory    | Untouched by Systhema                 | `src/blocks/`, `src/components/`, `src/lib/`, `src/access/`, `src/app/about/page.tsx`, anything you author                                            |

### With frontend locales enabled

Configuring [`locales`](https://docs.systhema.app/fr/next/nextjs/locales.md) adds no glue files, `src/proxy.ts` and the catch-all `page.tsx` stay byte-identical. The one file that changes shape is the site shell, and the tiers still hold:

| File                                              | Tier              | Notes                                                                                                |
| ------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------- |
| `src/app/(site)/shell.tsx`                        | Starter scaffold  | Replaces `template.tsx`. Your fonts, branding, header/footer, receives the route `locale` as a prop. |
| `(site)/(systhema)/[[...segments]]/layout.tsx`    | Framework-managed | Boundary layout: resolves the route locale and injects it into your shell.                           |
| `(site)/(systhema)/[[...segments]]/not-found.tsx` | Framework-managed | Re-exports your `(site)/not-found.tsx` so a thrown `notFound()` renders inside the shell.            |

Your 404 stays where it was, `src/app/(site)/not-found.tsx` is still the file you edit. Disabling locales reverses all of this and restores `template.tsx`.

## Project shapes

The three shapes match the HTML, Next.js and Payload [starter templates](https://docs.systhema.app/fr/next/getting-started/templates.md). Match the template, don't invent your own layout.

### HTML

```text
project-root/
├── index.html                      # entry — loads dist/style.css
├── package.json
├── systhema.config.js              # JS variant (HTML projects don't have TS)
└── src/
    ├── style.css                   # @import '@systhemaui/core/tailwind'
    └── tokens/                     # design tokens (your territory)
```

No router, no app shell. The HTML shape only consumes `@systhemaui/core`.

### Next.js without CMS

```text
project-root/
├── next.config.ts
├── package.json
├── systhema.config.ts
├── public/
└── src/
    ├── app/
    │   ├── globals.css             # @import '@systhemaui/core/tailwind'  ← starter scaffold
    │   ├── layout.tsx              # RootLayout — fonts, metadata, providers  ← starter scaffold
    │   └── page.tsx                # home page — your territory
    ├── blocks/                     # custom blocks — your territory (optional)
    ├── components/                 # custom React components — your territory (optional)
    ├── lib/                        # shared utilities — your territory (optional)
    └── tokens/                     # design tokens
```

Flat `src/app/`, no route groups. Add `src/blocks/`, `src/components/`, `src/lib/` when you need them. There is no `(site)/` segment in a Next-only project; Payload is the reason `(site)/` exists in the next shape.

### Next.js + Payload

```text
project-root/
├── next.config.ts                       # wraps in withPayload(...)
├── register-env.sh                      # loads .env into shell for systhema-core CLI
├── .env / .env.example                  # PAYLOAD_SECRET, DATABASE_URI, NEXT_PUBLIC_SERVER_URL
├── public/                              # static assets + uploads/ symlink
├── storage/                             # SQLite database + filesystem upload store
└── src/
    ├── payload.config.ts                # buildConfig wrapped in withSysthema(...)
    ├── payload-types.ts                 # generated by `payload generate:types`
    ├── proxy.ts                         # systhemaGateway(...)  ← framework-managed
    ├── tokens/                          # design tokens
    ├── blocks/                          # custom blocks — your territory (see "Block mirror pattern")
    ├── components/                      # custom React components — your territory
    ├── lib/                             # shared utilities — your territory
    ├── access/                          # custom capabilities + access functions — your territory
    └── app/
        ├── layout.tsx                   # outer layout — starter scaffold
        ├── robots.txt/route.ts          # host-aware robots.txt — starter scaffold
        ├── (payload)/                   # Payload admin segment — Payload's, written once
        │   ├── layout.tsx
        │   ├── custom.scss
        │   ├── admin/
        │   └── api/
        └── (site)/                      # public-facing site
            ├── globals.css              # @import '@systhemaui/core/tailwind'  ← starter scaffold
            ├── template.tsx             # RootLayout + RootHeader + RootFooter  ← starter scaffold
            ├── not-found.tsx            # 404 page — starter scaffold
            ├── (systhema)/              # CMS-driven routes — framework-managed
            │   ├── [[...segments]]/page.tsx
            │   ├── (sitemaps)/sitemap.xml/route.ts
            │   └── sys/[route]/route.ts
            └── about/page.tsx           # custom static page — your territory (example)
```

The `(site)/` segment exists so the public site can coexist with the `(payload)/` admin segment; both share `src/app/layout.tsx`. Your custom pages mount under `(site)/` so they inherit `template.tsx`'s header/footer.

### What's shared across all three

- `src/tokens/`, always, every shape. Tokens are the design-system source of truth.
- `systhema.config.{ts,js}` at the project root, always, every shape.
- "Your territory" folders (`src/blocks/`, `src/components/`, `src/lib/`, etc.), always optional, always your call to create.

## Block mirror pattern

A Systhema custom block has two halves: a Payload **schema** and a React **component**. Colocate them in one folder rather than splitting them across `src/blocks/` and `src/components/`:

```text
src/blocks/Hero/
├── index.ts            # Payload schema — fields, lexical features, customBlocks registration
└── component.tsx       # React renderer — receives Payload props, returns JSX
```

Register the block objects through `buildConfig(withSysthema(baseConfig, options))`, with `options.customBlocks` containing each schema and converter. See [Custom blocks](https://docs.systhema.app/fr/next/payload/custom-blocks.md) for the complete registration contract. The same colocation works in Next-only projects. `src/blocks/Hero/component.tsx` can be imported and rendered with hardcoded props until Payload arrives. The `systhema:building-sites` agent skill covers the pattern in detail.

## `lib/` over `utils/`

One name for shared utilities: **`src/lib/`**, never `src/utils/`. Backend helpers, shared formatters, request/response builders all live here. Frontend-only utilities can colocate next to the consuming component (`src/components/foo/utils.ts`) or share via `src/lib/` if cross-cutting.

This avoids the most common drift in real Systhema projects, half use `lib/`, half use `utils/`, a few use both.

## Upgrading from Next-only to Next + Payload

Adding Payload to a Next-only project is **mostly additive**. Existing `src/blocks/`, `src/components/`, `src/lib/` survive untouched; the integration adds Payload's app-router segment and the framework-managed routes.

| Step                                                                           | What changes                                                                                                                                                                                            |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1. Install packages                                                            | Add `@systhemaui/payload`, `payload`, db driver (`@payloadcms/db-sqlite`, `@payloadcms/db-postgres`, …).                                                                                                |
| 2. Create `src/payload.config.ts`                                              | New file. Wrap `buildConfig({...})` in `withSysthema(...)`. Set `secret`, `db`.                                                                                                                         |
| 3. Move `src/app/page.tsx` → `src/app/(site)/page.tsx`, same for `globals.css` | The only file-move step. Everything else is additive.                                                                                                                                                   |
| 4. Run `systhema-core payload create-app-files`                                | Scaffolds `src/proxy.ts`, `src/app/(site)/(systhema)/`, `src/app/(site)/template.tsx`, `not-found.tsx`, and `src/app/(payload)/` when the project has none. Then run `payload generate:importmap`.      |
| 5. Wire existing custom blocks                                                 | Add `CustomBlock` objects to `customBlocks: [myBlock]` in the plugin options. Each object provides `type`, `data`, `editor` and `converter`; register browser converters separately for client preview. |
| 6. Set `packages: { payload: true }` in `systhema.config.ts`                   | Activates Payload-specific token outputs and admin-UI utilities.                                                                                                                                        |
| 7. Run `pnpm sync`                                                             | Regenerates artifacts and the configured Payload outputs. If you changed package versions, review the separate upgrade and database migration plan.                                                     |

The full guide, including diagnosis of what survives, the `customBlocks` wiring, and token migrations, is in the `systhema:building-sites` agent skill.

## Customizing the content model

The editor's content model is the page. Start from the assumption that every page is a `default` page (the `hero` group plus one root `content` field composed from native blocks) and that the home page can use the same default template. Add a custom template only when its shell or data requirements differ. Add schema only where the system has no answer, and decide top down:

| You need…                                                 | Use                                                                                                                                                                                                                               |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A page                                                    | A `default` page composed from native blocks.                                                                                                                                                                                     |
| A band repeated on several pages                          | `components: true` and the `component` block.                                                                                                                                                                                     |
| A page whose renderer needs data or a bespoke layout      | A custom page template: one root `content` field, `resolveData` for the fetch. A listing page reuses the archive's `editorTabs` shape and `createPostsListingFields()`.                                                           |
| A kind of post                                            | `posts.types` plus a thin post template (root `content` and the two or three typed fields the kind cannot render without).                                                                                                        |
| A band no native block renders, or one that runs a query  | A custom block, registered at the editors its nearest native block uses, using the same tier as the section or region its renderer supplies. Add design controls only when the native composition cannot express the requirement. |
| Data with its own identity, read from more than one place | A collection via `customCollections`.                                                                                                                                                                                             |

Typed template fields are for what the page breaks without (a listing's config, a hero, an embed URL); prose is blocks. Custom blocks used on exactly one page, typed prose fields, single-reader collections and a settings global that duplicates the Footer global are the shapes that get rebuilt later. `withSysthema` replaces the base config's `collections` and `globals` arrays with its own set plus `customCollections` / `customGlobals`; extend a Systhema-owned collection after the call, not by passing `collections`.

## When in doubt

- File is in `src/app/(site)/(systhema)/`: framework-managed, keep your application code outside it. `src/proxy.ts` is also managed; preserve project security changes and review its `.new` file during upgrades.
- File is in `src/app/(payload)/` → **Payload's route group**, never rewritten by upgrades; edit only `custom.scss`.
- File has a "Starter file scaffolded by Systhema" banner → **starter scaffold**, edit freely.
- File is anywhere else under `src/` → **your territory**.

The [starter templates](https://docs.systhema.app/fr/next/getting-started/templates.md) are the canonical layouts. Match them; don't invent.
