Directus plus Next.js in production — what I actually wire up
The exact Directus + Next.js stack I ship on client projects: schema patterns, ISR strategy, image handling, and the guardrails that keep the token off the browser.
Directus has quietly become my default headless CMS on client projects. Not because it is the flashiest option — Sanity and Payload are excellent — but because it is self-hostable, SQL-first, and gives editors an admin that they can navigate without a two-hour onboarding call.
Below is the exact production stack I wire up on a fresh Directus plus Next.js engagement, and the guardrails that stop the classic mistakes.
Schema patterns that survive year two
Every collection has an explicit
status enum (draft, published, archived), a
sort integer for editorial ordering, and a
slug unique index. That trio covers ninety percent
of real editorial requirements — no bespoke workflow plugins,
no custom hooks.
Reference relationships are always modelled explicitly, never as JSON blobs. If a service belongs to a category, that is a real M2O — because six months in, the editors want a list of services filtered by category, and JSON blobs cannot be indexed.
The token never touches the browser
The single most common Directus mistake I audit out of client
codebases: putting the admin token in a
NEXT_PUBLIC_ env var. That ships it to every
browser hitting your site. Instead, I put the token in
DIRECTUS_ADMIN_TOKEN (no
NEXT_PUBLIC_) and import a
directus.ts module that starts with
import "server-only". Node throws at build time if
any client component accidentally imports it.
ISR — not client-side fetching
Every Directus read is a server-side fetch with
next: { revalidate: 60 } — never a
client component calling the API on mount. This does three
things at once: the content is in the initial HTML (so search
engines and LLMs see it on first byte), the token stays
server-side, and CDN caching does most of the traffic work.
Fallback to static content when Directus is unreachable
Directus is self-hosted. Self-hosted services have bad days. On every project I ship a static fallback array in the codebase that the server-side fetcher returns when Directus is unreachable. The site never white-screens because the CMS is rebooting.
Image handling — Directus assets plus next/image
Directus files get served from
DIRECTUS_URL/assets/{file_id}. That
hostname goes into
next.config.ts under
images.remotePatterns, and every asset renders
through next/image with explicit width and height
— same as any CDN image. Do not fetch Directus files
client-side to render them; Next.js will do the AVIF/WebP
pipeline for you.
The permission model most people get wrong
In Directus 11, permissions live under policies, not roles directly. The public role gets a policy that grants read on published items only — that is one filter row per collection. The admin token stays admin. Anything else is a footgun.
What Directus is bad at
Not everything. Content localisation on nested relations is still fiddly; the visual builder is not Sanity Studio; and heavy media libraries are better served by a dedicated DAM. For most B2B and small-to-mid B2C storefronts, none of that matters. For a five-language global brand with a video-heavy library, it might.