Docs
Systhema Design (opens in new tab)
Changelog

v1.5.0

The new global `systhema` CLI, a `[[...segments]]` catch-all with configurable sitemaps, capability-based access control, captcha verification and the Systhema Figma plugin.

On this page

๐Ÿ†• New Global CLILink to this section

This release introduces @systhemaui/cli, the new global Systhema CLI. Install it once and use systhema create, systhema upgrade, systhema codemod, systhema info, systhema self-update from anywhere:

pnpm add -g @systhemaui/cli
systhema --help

The global CLI also proxies the project-local helpers (sync, init, payload <sub>, etc.), so you no longer need pnpm exec for most things. See the CLI guide for the full command list.

Bin rename: the project-local systhema bin (provided by @systhemaui/core) was renamed to systhema-core so the systhema name belongs to the new global CLI. systhema upgrade runs a codemod (rename-systhema-bin) that rewrites package.json scripts automatically โ€” manual action only needed if you have custom invocations elsewhere.


โš ๏ธ Must Be AdaptedLink to this section

These changes will break your project if not addressed. Apply them before upgrading.

1. Migrate to [[...segments]] Catch-All & New Sitemaps (#28 (opens in new tab))Link to this section

The (systhema) route group has been consolidated from 6 consumer files down to 3. The old [...path] structure is no longer supported โ€” you must migrate to the new [[...segments]] catch-all.

Before (6 files)Link to this section

(systhema)/
โ”œโ”€โ”€ page.tsx                              โ† homepage
โ”œโ”€โ”€ [...path]/page.tsx                    โ† CMS pages
โ”œโ”€โ”€ components/[id]/page.tsx              โ† component previews
โ”œโ”€โ”€ (sitemaps)/
โ”‚   โ”œโ”€โ”€ sitemap.xml/route.ts              โ† sitemap index
โ”‚   โ””โ”€โ”€ pages-sitemap.xml/route.ts        โ† pages sitemap
โ””โ”€โ”€ sys/[route]/route.ts                  โ† API routes

After (3 files)Link to this section

(systhema)/
โ”œโ”€โ”€ [[...segments]]/page.tsx              โ† all pages, components, future modules
โ”œโ”€โ”€ (sitemaps)/sitemap.xml/route.ts       โ† single sitemap.xml
โ””โ”€โ”€ sys/[route]/route.ts                  โ† API routes + grouped sitemap sub-routes

How to migrateLink to this section

The fastest path โ€” regenerate all files automatically:

# Delete old page files
rm src/app/(site)/(systhema)/page.tsx
rm -r src/app/(site)/(systhema)/\[...path\]
rm -r src/app/(site)/(systhema)/components

# Delete old sitemap files
rm -r src/app/(site)/(systhema)/\(sitemaps\)/pages-sitemap.xml
# If you had a manually created sitemap index, remove it too:
# rm src/app/(site)/(systhema)/(sitemaps)/sitemap.xml/route.ts

# Regenerate everything
systhema payload create-app-files --override   # global CLI (proxies to project-local)
# or, equivalently:
pnpm exec systhema-core payload create-app-files --override

Or see the template project (opens in new tab) for the exact file contents if you prefer to migrate manually.

Sitemaps must also be migratedLink to this section

The old manually-maintained sitemap route files (pages-sitemap.xml/route.ts, etc.) are replaced by a single sitemap.xml/route.ts that delegates to the library. If your project had custom sitemaps (e.g., for blog posts or projects), migrate them to the config-based approach:

Before (manual route files):

(sitemaps)/
โ”œโ”€โ”€ sitemap.xml/route.ts              โ† manually lists sub-sitemaps
โ”œโ”€โ”€ pages-sitemap.xml/route.ts        โ† manually queries pages
โ””โ”€โ”€ projects-sitemap.xml/route.ts     โ† manually queries projects

After (config-based, single file):

(sitemaps)/
โ””โ”€โ”€ sitemap.xml/route.ts              โ† one file, delegates to library

Custom entries are now defined in the plugin config โ€” see Configurable Sitemaps under New Features for full examples.

What you getLink to this section

  • SSG by default โ€” all pages pre-rendered at build time with ISR revalidation (~1 month). Admin bar auth moved to client-side, removing the dynamic API calls that previously prevented static generation.
  • Draft page protection โ€” queryPageByPath now explicitly filters _status: 'published' for public visitors. Draft pages were previously accessible.
  • Configurable sitemaps โ€” add custom entries or grouped sub-sitemaps via plugin config instead of manual route files.
  • Future-proof โ€” when Systhema Modules land, your page files won't need any changes.

2. withSysthema() Plugin Options Overhaul (#23 (opens in new tab))Link to this section

Several features that were previously enabled by default now require explicit opt-in:

FeatureBeforeAfterHow to enable
componentsenableddisabledcomponents: true
googleMapstrue / falsefalsegoogleMaps: { apiKey: '...' }
embedenableddisabledembed: true
formsenableddisabledforms: true or forms: { fields: { ... } }
redirectenableddisabledredirect: true
emailsauto (with adapter)falseemails: { defaultFromAddress: '...' }

googleMaps no longer accepts true โ€” the API key is now part of the config:

// Before
googleMaps: true
// After
googleMaps: { apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY }

emails no longer accepts true โ€” env vars are no longer read internally:

// Before
emails: true  // read from SYSTHEMA_EMAIL_* env vars
// After
emails: {
  defaultFromAddress: process.env.SYSTHEMA_EMAIL_FROM!,
  defaultReplyTo: process.env.SYSTHEMA_EMAIL_REPLY_TO,
  adminAddress: process.env.SYSTHEMA_EMAIL_ADMIN_ADDRESS,
}

APP_ENVIRONMENT env var removed โ€” use the environment plugin option:

environment: (process.env.APP_ENVIRONMENT as 'production' | 'staging' | 'development') || 'production',

VERCEL_PROJECT_PRODUCTION_URL removed โ€” NEXT_PUBLIC_SERVER_URL is the only URL source.

Full migration exampleLink to this section

// Before (v1.4.x) โ€” everything enabled by default, env vars read internally
withSysthema(config, {})

// After (v1.5.0)
withSysthema(config, {
  environment: (process.env.APP_ENVIRONMENT as 'production' | 'staging' | 'development') || 'production',
  components: true,
  embed: true,
  redirect: true,
  googleMaps: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY
    ? { apiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY }
    : false,
  emails: process.env.SYSTHEMA_EMAIL_FROM
    ? {
        defaultFromAddress: process.env.SYSTHEMA_EMAIL_FROM,
        defaultReplyTo: process.env.SYSTHEMA_EMAIL_REPLY_TO,
        adminAddress: process.env.SYSTHEMA_EMAIL_ADMIN_ADDRESS,
      }
    : false,
  forms: true,
})

3. Minimum Dependency VersionsLink to this section

PackageOld minimumNew minimum
tailwindcss^4.1^4.2 (#15 (opens in new tab))
@payloadcms/*^3.79.1^3.80.0 (#16 (opens in new tab))

4. Payload 3.80: imageURL โ†’ admin.images.icon (#17 (opens in new tab))Link to this section

Payload deprecated imageURL in 3.79.0. All 41 usages across Systhema have been migrated. If you have custom blocks using imageURL, update them:

  const MyBlock: Block = {
    slug: 'myBlock',
-   imageURL: '/icons/my-block.svg',
+   admin: { images: { icon: '/icons/my-block.svg' } },
    fields: [/* ... */],
  }

5. next lint Removed in Next.js 16 (#29 (opens in new tab))Link to this section

- "lint": "next lint",
+ "lint": "eslint .",

Not immediately breaking, but you should do these now. Skipping them will cause problems in a future release.

Migrate Legacy Token Format (#12 (opens in new tab))Link to this section

The old community plugin ("Design Tokens Manager") token format is now deprecated and will be removed in a future major release. If your project still uses old-format token files, migrate now โ€” either by re-exporting from Figma using the new @systhemaui/figma plugin, or by running:

systhema migrate-tokens

Both formats currently produce identical CSS output, so migration is safe. But legacy support will be dropped โ€” don't wait.

Add Systhema Gateway (#27 (opens in new tab))Link to this section

The gateway is an extensible request middleware that will gain features (i18n locale detection, maintenance mode) without requiring consumer changes. Setting it up now means you're ready when those features land.

src/proxy.ts (or src/middleware.ts for Next.js <16):

import { systhemaGateway } from '@systhemaui/payload/gateway'

export const proxy = systhemaGateway()

If you already use a custom admin domain, pass the config:

export const proxy = systhemaGateway({
  customAdminURL: process.env.ADMIN_URL,
})

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Next.js Config Improvements (#29 (opens in new tab))Link to this section

Rename next.config.mjs โ†’ next.config.ts with proper TypeScript typing and the new devBundleServerPackages: false option from the official Payload template:

import { withPayload } from '@payloadcms/next/withPayload'
import type { NextConfig } from 'next'

const nextConfig: NextConfig = { /* ... */ }

export default withPayload(nextConfig, { devBundleServerPackages: false })

Remove exprContextCritical workaround if your next.config has it โ€” this was a temporary fix for noisy webpack warnings from Payload's job queue and is no longer needed:

- config.module.exprContextCritical = false

Install @payloadcms/typescript-plugin (#18 (opens in new tab))Link to this section

Now a peer dependency. Provides IDE support for Payload component paths. Add to your tsconfig.json:

{
  "compilerOptions": {
    "plugins": [{ "name": "@payloadcms/typescript-plugin" }]
  }
}

โœจ New FeaturesLink to this section

If your website already has a custom solution for any of these, consider migrating to the built-in version.

Capability-Based Access Control (#7 (opens in new tab))Link to this section

Replaces the hardcoded admin/editor role checks with a flexible, fine-grained permission system. If you've built custom role logic, this replaces it entirely.

Capability String FormatLink to this section

Capabilities use dot-notation to express resource-level, field-level, and global-level permissions:

{collection}.{operation}           โ†’ pages.create, uploads.delete
{collection}.{field}.{operation}   โ†’ pages.seo.update, users.roles.read
global.{global}.{operation}        โ†’ global.header.update, global.seo.read

Wildcards are supported: * (all capabilities), pages.* (all pages capabilities including field-level), global.* (all global capabilities).

Built-in RolesLink to this section

RoleSlugDescription
DeveloperdevFull access (*). Invisible to non-dev users. Assigned via pnpx payload seed-dev only.
AdminadminAll built-in Systhema features. Custom collections are NOT included by default.
EditoreditorRead + update content as drafts. Full uploads. Read-only globals.
SEO ManagerseoManagerRead pages with editable SEO fields only. Full global SEO access.

Since admin is scoped to built-in features only, custom collections are dev-only by default. Use overrideRoles to grant admin (or editor) access to project-specific collections:

withSysthema(config, {
  roles: {
    // Register custom capabilities
    customCapabilities: [
      'projects.create', 'projects.read', 'projects.update', 'projects.delete',
    ],
    // Create custom roles
    customRoles: [
      { name: 'contentReviewer', label: 'Content Reviewer', capabilities: ['pages.read', 'components.read'] },
    ],
    // Extend built-in roles
    overrideRoles: (defaults) => ({
      ...defaults,
      admin: {
        ...defaults.admin,
        capabilities: [...defaults.admin.capabilities, 'projects.*', 'tickets.*'],
      },
      editor: {
        ...defaults.editor,
        capabilities: [...defaults.editor.capabilities, 'pages.create', 'pages.delete', 'pages.publish'],
      },
    }),
  },
})

To restore the old behavior where admin sees everything (including all custom collections):

overrideRoles: (defaults) => ({
  ...defaults,
  admin: { ...defaults.admin, capabilities: ['*'] },
})

Scoped Field-Level UpdatesLink to this section

Sub-capabilities like pages.seo.update allow editing specific field groups while keeping everything else read-only. A user with only pages.seo.update can see pages, save pages, but only edit the SEO tab โ€” all other fields appear read-only in the admin UI.

For consumer collections, apply the same pattern:

import { requireUpdateOrScoped, addScopedFieldAccess, requireCapabilityField } from '@systhemaui/payload'

const Articles: CollectionConfig = {
  slug: 'articles',
  access: { update: requireUpdateOrScoped('articles') },
  fields: addScopedFieldAccess([
    { name: 'title', type: 'text' },
    { name: 'content', type: 'richText' },
    {
      name: 'metadata',
      type: 'group',
      // Users with articles.metadata.update can edit this without full articles.update
      access: { update: requireCapabilityField('articles.metadata.update') },
      fields: [/* ... */],
    },
  ], 'articles.update'),
}

Per-User CapabilitiesLink to this section

A capabilities multi-select field on Users allows assigning individual capabilities per-user โ€” extend a role, start blank with specific capabilities, or use wildcards. Capabilities covered by roles are auto-removed on save. Escalation prevention ensures users can only assign capabilities they possess.

New Public API ExportsLink to this section

Access function generators: requireCapability(), requireCapabilityField(), requireAnyCapability(), requireUpdateOrScoped(), publicOrCapability(), capabilityOrPublished(), capabilityOrSelf(), addScopedFieldAccess()

Capability utilities: hasCapability(), hasAnyCapability(), getUserCapabilities(), isDev(), getRoleCapabilities(), getAllRoleDefinitions(), getRoleOptions(), getCapabilityOptions()

Legacy access functions: admin, editor, authenticated, adminOrSelf, adminsOrPublished, authenticatedOrPublished, anyone, nobody, checkRole โ€” all still work.

Backwards CompatibilityLink to this section

No data migration needed โ€” admin/editor role strings are preserved. checkRole() and user.roles?.includes('admin') patterns still work. Without the roles config option, behavior is identical to before.


Systhema Figma Plugin & Dual Token Format (#12 (opens in new tab))Link to this section

If you're still using the abandoned "Design Tokens Manager" plugin, this replaces it with a first-party @systhemaui/figma plugin. Full round-trip token sync between Figma and code.

  • Export: Single-click export of all Figma variables and styles as W3C DTCG-compliant token files
  • Import: Full round-trip sync with granular conflict resolution โ€” dry-run analysis, mode mapping, per-variable Replace/Keep/Add/Skip actions, three-pass alias resolution
  • Core Dual Format: New format is primary. Old format works via deprecated legacy adapter. Both produce identical CSS output.
# Migrate old tokens in-place
systhema migrate-tokens

Custom Admin URL & AdminBar Extensibility (#27 (opens in new tab))Link to this section

If you've set up a custom admin domain or extended the AdminBar yourself, Systhema now handles this natively.

withSysthema(config, {
  customAdminURL: process.env.ADMIN_URL,
  customLivePreviewURL: process.env.LIVE_PREVIEW_URL,
  whitelabel: {
    enabled: true,
    graphics: {
      Logo: '@/components/MyLogo',
      Icon: '@/components/MyIcon', // shared between admin panel and frontend AdminBar
    },
  },
  customCollections: [CaseStudies, PressReleases], // labels auto-extracted for AdminBar
})

Auto-configures routes, CSRF, cross-domain cookies, and live preview URL pinning. Zero overhead for logged-out visitors. The Systhema Gateway handles admin domain routing.

Note: Cross-domain admin does not work with localhost subdomains. Use a real domain alias for local development.


Configurable Sitemaps (#28 (opens in new tab))Link to this section

If you have custom sitemap route files, replace them with config-based sitemaps. With no config, /sitemap.xml auto-includes all published CMS pages.

Default mode (single flat sitemap)Link to this section

withSysthema(baseConfig, {
  sitemaps: {
    additional: [
      // Static entry
      { url: '/login' },
      // Dynamic resolver from a collection
      async (payload) => {
        const posts = await payload.find({ collection: 'projects', limit: 1000 })
        return posts.docs.map(p => ({ url: `/projects/${p.slug}`, lastModified: p.updatedAt }))
      },
    ],
  },
})

Grouped mode (sitemap index + sub-sitemaps)Link to this section

When grouped: true, generates a sitemap index at /sitemap.xml with sub-sitemaps served via /sys/:

withSysthema(baseConfig, {
  sitemaps: {
    grouped: true,
    additional: [
      { url: '/login' },                                // โ†’ /sys/pages-sitemap.xml
      { url: '/register' },                              // โ†’ /sys/pages-sitemap.xml
      { group: 'projects', entries: async (payload) => { // โ†’ /sys/projects-sitemap.xml
        const posts = await payload.find({ collection: 'projects', limit: 1000 })
        return posts.docs.map(p => ({ url: `/projects/${p.slug}` }))
      }},
    ],
  },
})

Entries use the full Next.js MetadataRoute.Sitemap shape (url, lastModified, changeFrequency, priority, alternates, images, videos). All sitemap responses include Cache-Control: public, s-maxage=3600, stale-while-revalidate=600.

New exportsLink to this section

ExportPackage
systhemaSitemapRoute@systhemaui/payload/next
systhemaSitemapGroupRoute@systhemaui/payload/next
SitemapEntry, SitemapResolver, SitemapAdditionalEntry, SitemapsConfig@systhemaui/payload (types)

Captcha Verification โ€” reCAPTCHA & Turnstile (#20 (opens in new tab))Link to this section

If you've integrated captcha manually into your forms, Systhema now supports Google reCAPTCHA v2 and Cloudflare Turnstile out of the box. Both can be configured simultaneously.

withSysthema(config, {
  forms: {
    fields: {
      recaptcha: {
        siteKey: process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!,
        secretKey: process.env.RECAPTCHA_SECRET_KEY!,
      },
      turnstile: {
        siteKey: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!,
        secretKey: process.env.TURNSTILE_SECRET_KEY!,
      },
    },
  },
})

Captcha blocks appear as form field types in the admin. Frontend widget loads on demand with skeleton loading, blocks submission until completed. Server-side verification happens automatically.


User Avatar, Upload Ownership & FileGuard (#31 (opens in new tab))Link to this section

If you've added custom upload ownership or file validation, Systhema now provides these built-in.

User Avatar โ€” optional upload field on Users for profile pictures, rendered in the Payload admin top bar. Enabled by default (users: { avatar: false } to disable).

Upload Ownership โ€” every upload tracks who created it. Users without full upload capabilities can still manage their own files:

CapabilityReadCreateUpdateDelete
None (basic user)Own onlyYesOwn onlyOwn only
uploads.readAllYesOwn onlyOwn only
uploads.updateAllYesAllOwn only
uploads.deleteAllYesAllAll

FileGuard (renamed from FileSizeGuard) โ€” validates both file size AND MIME type client-side:

withSysthema(config, {
  uploads: {
    maxFileSize: 50 * 1024 * 1024,
    allowedMimeTypes: ['image', 'application/pdf'],
  },
})

Custom Error Messages & Checkbox Required Modes (#21 (opens in new tab))Link to this section

Form fields now support custom error messages via Advanced Settings. Checkbox groups have a configurable required mode: Each option (all must be checked) or At least one.


strictDraftTypes (#19 (opens in new tab))Link to this section

The default config now enables strictDraftTypes: true, generating stricter TypeScript types that distinguish between draft and published document shapes.


Emails Toggle (#11 (opens in new tab))Link to this section

If you manage emails independently (custom templates, different env vars), disable Systhema's email features:

withSysthema(config, { emails: false })

New TailwindCSS Color Tokens (#14 (opens in new tab))Link to this section

Added mauve, olive, mist, and taupe color tokens matching TailwindCSS v4.2. Available in Figma designs and project variable resolution.


๐Ÿ› Fixes & Internal ImprovementsLink to this section

Bug FixesLink to this section

  • Disabled buttons now correctly show the default color variant instead of hover colors (#13 (opens in new tab))
  • Form relationships inside Lexical blocks are now properly populated โ€” forms were only rendering in preview mode before (#25 (opens in new tab))
  • Server-only modules no longer leak into client bundles when running in workspace mode โ€” fixes 500 errors caused by sharp, revalidatePath, and @payloadcms/richtext-lexical internals (#24 (opens in new tab))
  • Draft pages are no longer visible to public visitors โ€” explicit _status: 'published' filter added (#28 (opens in new tab))
  • Live preview hydration mismatch fixed by initializing isLivePreview from search params (#28 (opens in new tab))
  • Token asterisks in the new Figma plugin format are now stripped from token names and references โ€” fixes CSS build errors like var(--color-bunker-950*) (#32 (opens in new tab))
  • Uploads public read access restored โ€” uploads were incorrectly restricted to authenticated users only (#33 (opens in new tab))
  • AdminBar capability checks โ€” Edit and New links are now hidden for users without the required capabilities (#34 (opens in new tab))
  • Email capabilities no longer registered when the Emails global is disabled โ€” prevents phantom capabilities from appearing in role configuration (#35 (opens in new tab))
  • Payload template โ€” replaced middleware.ts with proxy.ts for Next.js 16 and fixed Invalid URL error from missing NEXT_PUBLIC_SERVER_URL (#36 (opens in new tab))
  • seed-dev script now uses Payload's bin script API instead of importing @payload-config directly โ€” fixes build errors in consumer projects (#37 (opens in new tab))
  • AdminBar collection override โ€” new collection prop allows explicitly setting the collection slug, useful when auto-detection fails on custom page types (#38 (opens in new tab))
  • "Block not found" during HMR โ€” Lexical editor block resolution now uses a stable lookup map instead of fragile array indexing, fixing crashes during hot module replacement (#39 (opens in new tab))
  • Plugin stores survive HMR โ€” custom blocks, text states, and plugin options stores now persist on globalThis to prevent state loss during hot module replacement; reverts editor changes from #39 in favor of this approach (#40 (opens in new tab))
  • Block _sanitized flag cleared on HMR โ€” sanitized blocks no longer skip re-initialization after hot module replacement, fixing stale block configuration (#41 (opens in new tab))
  • Source maps no longer published โ€” react, next, and payload packages were shipping .js.map and .d.ts.map files, exposing original TypeScript source. Source map emission is now disabled in both tsup and tsc configs (#42 (opens in new tab))
  • ESLint warnings resolved across the payload package โ€” unstable useMemo deps, missing useEffect dep, unused import (#26 (opens in new tab))
  • API URL hidden from all collections and globals in the admin panel
  • Type generation now uses tsx watch in dev mode for faster iteration

Internal / MonorepoLink to this section

  • Workspace simplification (#29 (opens in new tab)) โ€” templates are now proper workspace members, pnpm use:workspace / pnpm use:published scripts removed
  • Template cleanup (#23 (opens in new tab)) โ€” example code moved to examples/ directories
  • CI โ€” GitHub Actions cleaned up, duplicate build eliminated, Node 22, concurrency controls
  • Design tokens re-exported and updated across all templates (#30 (opens in new tab))
  • Registry cleanup โ€” all 1.0.0โ€“1.4.1 stable versions on GitHub Packages were re-published without source maps, and all leftover beta/canary/internal versions outside the 1.5.0 line have been deleted from the registry. Production sites using --frozen-lockfile against an affected version will need to refresh their lockfile, since tarball shasums changed.

List of all changesLink to this section

๐Ÿš€ FeaturesLink to this section

coreLink to this section

payloadLink to this section

generalLink to this section

โ™ป๏ธ RefactorsLink to this section

payloadLink to this section

๐Ÿ› Bug fixesLink to this section

coreLink to this section

payloadLink to this section

templates/payloadLink to this section

generalLink to this section

๐Ÿ’„ StyleLink to this section

generalLink to this section

๐Ÿ“š DocsLink to this section

generalLink to this section

โช RevertsLink to this section

coreLink to this section

๐Ÿงน ChoresLink to this section

coreLink to this section

payloadLink to this section

generalLink to this section