Directus Access Policies for Public and Preview Tokens

Within Directus Data Layer Patterns, this guide sets up the permissions a headless frontend needs and nothing more. It covers a read-only policy that only returns published items and public fields, a separate preview policy for drafts and content versions, the users and static tokens that carry them, and how to audit permissions as the schema grows.

Directus permissions are powerful and granular: per collection, per action, with item filters, field lists and validation. That granularity cuts both ways. A frontend that reads with an administrator token, or through a public role with broad permissions, exposes internal fields, draft content and sometimes other collections entirely, because the API returns whatever the permissions allow. Designing narrow policies for the frontend is one of the most valuable hours in a Directus project.

Policies for a headless frontendThree access levels for frontend use: a public or read-only policy for published items and public fields, a preview policy for drafts and versions used only by the server in draft mode, and editor and admin roles that are never used by the frontend.Read-only policydelivery, server or edgepublished itemspublic fields onlyPreview policydraft mode, server onlydrafts and versionssame fields + statusEditor roleshumans in the studiocreate, update, publishAdminnever used by the frontendeverything
The frontend only ever holds the first two, and the second only on the server.

The Problem

A company’s website read from Directus using a static token that belonged to an administrator account created during setup. The API responses for team members included email addresses, phone numbers and internal notes, which the frontend did not display but which were visible to anyone inspecting the page data. Draft job postings appeared briefly in listings whenever the build ran while an editor was drafting, because the admin token could read everything.

How to Structure Access

A read-only user with a narrow policy. Create a user, not a person, for the website, with a policy that allows only read on the collections the site renders. Add an item filter on each, such as status equals published and date_published before now, and select only the fields the frontend uses.

A separate preview user. Create a second user with a preview policy that can read drafts and content versions of the same collections, with the same field restrictions. Use its token only in draft-mode requests on the server.

Public role only if appropriate. The public role applies to unauthenticated requests. Use it for the read-only policy only when the API is meant to be public and rate-limited at the edge; otherwise keep the public role empty and use a token.

Static tokens on the server. Generate static tokens for both users and store them in the frontend’s server environment. Never put them in client bundles.

Permission settings per policyFor the read-only and preview policies, the allowed actions, item filters, field lists and where their tokens may be used.SettingRead-only policyPreview policyActionsreadreadItem filterstatus published, date not in futurenone, or not archivedFieldsfields the site renderssame fields plus statusVersionsnoreadToken useserver and edge fetchesdraft mode on the server only
The two policies differ only in which items they can see, never in which fields.

Implementation

Policies can be created in the studio and captured in schema snapshots, or created through the API in a setup script, which keeps them reviewable. The script below creates the read-only policy’s permissions for an articles collection.

TypeScript
// scripts/setup-permissions.ts: run once per environment with an admin token
import { createDirectus, rest, staticToken, createPolicy, createPermissions } from "@directus/sdk";

const admin = createDirectus(process.env.DIRECTUS_URL!).with(staticToken(process.env.DIRECTUS_ADMIN_TOKEN!)).with(rest());

const policy = await admin.request(createPolicy({ name: "Website read-only", app_access: false, admin_access: false }));

await admin.request(createPermissions([
  {
    policy: policy.id,
    collection: "articles",
    action: "read",
    fields: ["id", "title", "slug", "summary", "body", "date_published", "author", "hero_image"],
    permissions: { _and: [{ status: { _eq: "published" } }, { date_published: { _lte: "$NOW" } }] },
  },
  {
    policy: policy.id,
    collection: "authors",
    action: "read",
    fields: ["id", "name", "bio", "avatar"],          // no email, phone or internal notes
    permissions: {},
  },
  { policy: policy.id, collection: "directus_files", action: "read", fields: ["id", "title", "width", "height", "type"], permissions: {} },
]));

console.log(`Created policy ${policy.id}; attach it to the website user and generate a static token.`);

Attach the policy to a dedicated website user, generate a static token for that user and store it as DIRECTUS_READ_TOKEN on the frontend server. Repeat with a preview policy without the status filter, attached to a preview user.

Relations and field restrictions

Permissions apply per collection, including across relations. An article’s author relation returns only the author fields the policy allows, and nothing if the policy cannot read authors at all. That is what makes field restrictions effective, but it also means a missing permission on a related collection shows up as null relations in the frontend rather than as an error. When a relation unexpectedly comes back empty after a schema change, check the related collection’s permissions first.

Policies for multi-site and localized content

When one Directus instance serves several sites, brands or markets, access policies can enforce the boundaries between them. Give each site its own website user and read-only policy, filtered by a site field on every shared collection, so one site’s token can never read another site’s items, even when both live in the same tables. Translations follow the same logic: if some languages are published later than others, filter the translations relation by a per-language status so unpublished translations stay invisible to the public token. Keep these filters in the setup script, one policy per site generated from a list, rather than configured by hand, because a missing filter on a single collection is enough to leak content across sites. The multi-tenant patterns describe the wider architecture; the policy filters are its enforcement at the data layer, and the CI checks above should run once per site token.

Editors need attention too. Their roles should match their responsibilities: authors who create and edit drafts, publishers who can change status to published and promote versions, and administrators who manage the model. Keeping publishing rights separate from editing rights is what makes review workflows meaningful, and it keeps the permissions the frontend relies on, published status in particular, under the control of the people accountable for publication.

Configuration Reference

Item Recommendation Why
Website user dedicated, no studio access Not tied to a person.
Read-only filter published and not future-dated Drafts and scheduled items stay hidden.
Fields explicit list per collection Internal fields never leave the API.
Preview user separate policy and token Drafts only in draft mode.
Public role empty unless the API is public by design No accidental exposure.
Tokens server-side only, rotated No leaks through bundles.

Gotchas & Edge Cases

  • Wildcard fields. Allowing * on a collection exposes every field added later. Prefer explicit lists and review them when the schema changes.
  • Files collection. File metadata may include uploader and descriptions editors did not intend to publish. Restrict directus_files fields too.
  • Future-dated items. A status filter alone publishes scheduled items early; add a date filter or a scheduled status change.
  • Admin tokens in scripts. Setup and migration scripts need admin tokens; keep them in CI only, never in the frontend’s environment.

Worked Example

The company created a website user with a read-only policy limited to published, current items and to displayed fields, and a preview user for draft mode. Team member responses shrank to name, role and photo; job postings appeared only after publication; and a security review found no internal data in any public API response. When a new internal field was later added to job postings, it did not leak, because the read-only policy listed fields explicitly.

Fields exposed per team member responseNumber of fields returned for each team member by the website's API requests with an administrator token and with the read-only policy.Admin token23 fieldsRead-only policy4 fields
The frontend used four fields; the admin token returned all of them.

Auditing Permissions Over Time

Schemas grow, and permissions drift with them. Audit the website and preview policies whenever collections or fields are added, and on a schedule, for example quarterly. A useful automated check requests each routable collection with the read-only token and compares the returned field names against an allowlist committed in the frontend repository; any new field in the response fails the check until someone decides whether it should be public. Another check requests a known draft item with the read-only token and expects nothing. Both run in CI against staging and give early warning before an editor’s new field or a relaxed filter exposes data in production.

Rollout Checklist

  • Create a dedicated website user and a narrow read-only policy.
  • Filter items to published and current, and list fields explicitly.
  • Create a separate preview user and policy for draft mode.
  • Keep the public role empty unless the API is intentionally public.
  • Store static tokens on the server and rotate them.
  • Check exposed fields and draft visibility in CI.

Frequently Asked Questions

Why not use the public role for everything?

It is acceptable for truly public content with edge rate limiting in front of the API, but a token makes access explicit, revocable and easy to audit.

Do static tokens expire?

No, which is convenient for builds and servers. Rotate them manually on a schedule and when people with access leave, updating the frontend’s secret store at the same time.

Can the preview policy read everything?

It should read drafts of the same collections and fields, not more. Internal fields stay restricted even in preview.

How do permissions affect caching?

Responses for the read-only token are the same for everyone and can be cached publicly. Preview responses must never be cached anywhere, including the framework’s data cache.