ChaiBuilder Logo

Admin Route

By default the CMS admin panel and the visual builder both live under /admin. A single environment variable moves them to any path you choose, and every internal link, redirect, and generated URL follows. This page covers how to change it, how the change is wired through the app, and how to reference the admin path from your own code.

Change the route

# Custom admin path, no leading slash
PAYLOAD_ADMIN_ROUTE=cms-a8f3k2m9
  1. Set PAYLOAD_ADMIN_ROUTE in your hosting platform's environment settings, or in .env for local development.
  2. Rebuild and restart. The value is read when the Next.js config loads, so a running process will not pick it up. Locally, stop and re-run pnpm dev. In production, trigger a fresh deploy.
  3. Sign in at the new path, for example https://www.example.com/cms-a8f3k2m9.

You do not set NEXT_PUBLIC_PAYLOAD_ADMIN_ROUTE yourself. The build copies the resolved value into the client bundle so browser-side links resolve correctly. It exists only as a fallback for setups that cannot expose the server-side variable, and PAYLOAD_ADMIN_ROUTE wins whenever both are set.

Value rules

The value is normalized and validated at build time by parseAdminRoute:

  • No leading or trailing slash. Use cms-a8f3k2m9, not /cms-a8f3k2m9/. Stray slashes are stripped and repeated slashes collapsed rather than rejected.
  • At least 4 characters, otherwise the build fails with PAYLOAD_ADMIN_ROUTE must be at least 4 characters.
  • These characters are rejected: : ? * + ( ) { } [ ] | \. They collide with Next.js route matching, and the build fails with PAYLOAD_ADMIN_ROUTE contains unsupported characters. Letters, digits, hyphens, and underscores are the safe set.
  • Nested paths are allowed. internal/cms puts the panel at /internal/cms.
  • admin and an empty value both resolve to the default. Setting PAYLOAD_ADMIN_ROUTE=admin is the same as leaving it unset.

Because validation happens at build time, a bad value fails the deploy rather than shipping a broken panel.

What moves with it

Everything under the admin base path follows the resolved value:

Default With PAYLOAD_ADMIN_ROUTE=cms-a8f3k2m9
/admin - CMS dashboard /cms-a8f3k2m9
/admin/login /cms-a8f3k2m9/login
/admin/editor - visual builder /cms-a8f3k2m9/editor
/admin/api/revalidate /cms-a8f3k2m9/api/revalidate

Password reset emails and the robots.txt disallow list are generated from the same value, so both stay correct without extra work. Anything calling the revalidation route from outside the app needs its URL updated by hand, since that caller lives in your infrastructure rather than in the app. See Caching & Revalidation.

How it is wired

Four touch points read the resolved route. You do not need to edit any of them to change the path, but knowing where they are matters when you customize routing.

File Role
next.config.ts Publishes the value to the client bundle and adds the rewrites
payload.config.ts Sets Payload's own routes.admin
src/proxy.ts Redirects legacy /admin requests to the custom path
src/app/robots.ts Keeps the panel out of robots.txt

next.config.ts

The config exposes the value to the browser and rewrites the public path onto the real route folders. The rewrites are added only when a custom route is set, so the default deployment carries no extra routing work:

import { getAdminRouteSegment, isCustomAdminRoute } from './src/utilities/adminRoute'

const adminRouteSegment = getAdminRouteSegment()

const nextConfig: NextConfig = {
  env: {
    NEXT_PUBLIC_PAYLOAD_ADMIN_ROUTE: adminRouteSegment,
  },
  async rewrites() {
    if (!isCustomAdminRoute()) {
      return []
    }

    return [
      { source: `/${adminRouteSegment}`, destination: '/admin' },
      { source: `/${adminRouteSegment}/:path*`, destination: '/admin/:path*' },
    ]
  },
}

This is why the change is build-time: rewrites are resolved when the config loads.

payload.config.ts

Payload builds its own internal links from routes.admin, so it gets the same value:

import { getAdminRoute } from '@/utilities/adminRoute'

export default buildConfig({
  routes: {
    admin: getAdminRoute(),
  },
  // ...
})

src/proxy.ts

The proxy redirects requests that still hit /admin to the custom path, so bookmarks and older links keep working:

if (isCustomAdminRoute() && (pathname === '/admin' || pathname.startsWith('/admin/'))) {
  const url = request.nextUrl.clone()
  url.pathname = `${getAdminRoute()}${pathname.slice('/admin'.length)}`
  return NextResponse.redirect(url)
}

Note the security consequence: /admin redirects rather than returning a 404, so the custom path is discoverable by anyone who tries it. A custom route is defense-in-depth against bots and scanners, not access control. Authentication and roles are what protect the panel. See Roles & Permissions. If you want /admin to be a dead end instead, replace that redirect with a NextResponse.rewrite to your 404 page and tell your team the new URL directly.

Building admin URLs in your own code

Never hardcode /admin in custom blocks, panels, or server code. Build paths with the helpers so they follow the configured route.

In the starter app

src/utilities/adminRoute.ts is host-app code, deliberately free of framework and package imports so next.config.ts can load it through plain Node:

import { adminUrl, getAdminRoute, isCustomAdminRoute } from '@/utilities/adminRoute'

adminUrl()                      // '/cms-a8f3k2m9'
adminUrl('editor')              // '/cms-a8f3k2m9/editor'
adminUrl('api', 'revalidate')   // '/cms-a8f3k2m9/api/revalidate'
getAdminRoute()                 // '/cms-a8f3k2m9'
isCustomAdminRoute()            // true

Segments are joined with slashes and stray slashes on each segment are stripped, so adminUrl('/editor/') and adminUrl('editor') produce the same result.

These work on both the server and the client. On the client the value comes from NEXT_PUBLIC_PAYLOAD_ADMIN_ROUTE, which Next.js inlines at build time.

From the package

chaipro/payload exports the same logic for code that should not depend on starter internals. The URL builder is named defaultAdminUrl here rather than adminUrl:

import {
  DEFAULT_ADMIN_ROUTE,
  defaultAdminUrl,
  getAdminRoute,
  parseAdminRoute,
} from 'chaipro/payload'

Both implementations read the same two environment variables and stay in sync.

Overriding URL construction

If your app builds admin URLs differently, for example because a reverse proxy mounts the panel under a prefix the app never sees, pass your own adminUrl builder when configuring the Payload integration. It receives path segments and returns the final path:

adminUrl: (...segments) => ['/back-office', ...segments].join('/'),

Everything that constructs an admin link, including the CMS edit and create URLs the builder opens for collection items, routes through this function. Leave it unset and it falls back to defaultAdminUrl.

Do not rename the route folders

The custom path is a rewrite, not a move. On disk the route folders stay where they are:

src/app/(payload)/admin/[[...segments]]    # CMS panel
src/app/(builder)/admin/editor             # visual builder
src/app/(builder)/admin/api                # builder API routes

Renaming these directories to match your custom path breaks the rewrite targets and Payload's own route resolution. Change the environment variable and leave the folders alone.

Gotchas

  • The panel 404s after changing the value. The process was not restarted or the app was not rebuilt. The rewrites only exist in a build that saw the variable.
  • Links point at the old path in the browser. The client bundle carries a stale inlined value. Rebuild rather than only restarting the server.
  • The build fails on deploy. Check the value against the rules above. The error message names the specific rule that failed.
  • An external system gets a 404 on revalidation. Its URL still says /admin/api/revalidate. Update the caller.
  • Preview or editor redirects land on /admin/login. Some custom code is hardcoding the path. Replace it with adminUrl('login').

© ChaiBuilder. All rights reserved.