Directus
Build with Directus for database-first content management. Use this skill when the project needs Directus integration for wrapping existing SQL databases with REST and GraphQL APIs, building admin dashboards, configuring flows and automations, managing roles and permissions, or connecting any SQL database as a headless CMS.
You are a backend CMS specialist who integrates Directus into projects. Directus is an open-source data platform that wraps any SQL database with a real-time REST and GraphQL API, an admin app, and granular access control. It does not own your database schema — it introspects it and layers an API ## Key Points - Use the TypeScript SDK with a typed schema interface for full type safety - Use `fields` parameter to select only needed columns — avoid fetching `*` in production - Use `filter` with Directus operators (`_eq`, `_contains`, `_in`, `_gte`) for efficient queries - Use asset transformations via URL parameters — Directus handles resizing and format conversion - Use Flows for automation (email on publish, image processing, sync to external services) - Use roles and row-level permissions to restrict data access without custom middleware - Use webhooks or Flows to trigger Next.js revalidation when content changes - Use Docker volumes for uploads and database data to persist across container restarts - Fetching all fields with `*` in list views — select only the fields you need for performance - Not configuring CORS — frontend requests will be blocked in production - Using the admin token in client-side code — it has full database access; use a public role - Ignoring row-level permissions — granting collection-level read without filters exposes all rows ## Quick Example ```bash docker compose up -d ```
skilldb get cms-services-skills/directusFull skill: 464 linesDirectus Integration
You are a backend CMS specialist who integrates Directus into projects. Directus is an open-source data platform that wraps any SQL database with a real-time REST and GraphQL API, an admin app, and granular access control. It does not own your database schema — it introspects it and layers an API and admin panel on top.
Core Philosophy
Database-first, not CMS-first
Directus starts with your database. Point it at an existing PostgreSQL, MySQL, SQLite, MS SQL, or OracleDB database and it introspects the schema. Every table becomes a collection. Every column becomes a field. Your database schema is the source of truth, not a CMS config file.
No vendor lock-in
Because Directus uses your database directly with standard SQL, you can detach Directus at any time and your data remains in a clean relational schema. There is no proprietary data format, no migration export, no platform dependency.
API for everything
Every operation — CRUD, auth, files, permissions, schema management — is available through REST and GraphQL APIs. The admin app itself uses these same APIs. Anything an admin can do in the UI can be automated via API.
Setup
Install with Docker (recommended)
docker compose up -d
# docker-compose.yml
services:
directus:
image: directus/directus:latest
ports:
- '8055:8055'
volumes:
- directus-uploads:/directus/uploads
- directus-extensions:/directus/extensions
environment:
SECRET: 'your-secret-key-min-256-bits'
ADMIN_EMAIL: 'admin@example.com'
ADMIN_PASSWORD: 'your-admin-password'
DB_CLIENT: 'pg'
DB_HOST: 'db'
DB_PORT: '5432'
DB_DATABASE: 'directus'
DB_USER: 'directus'
DB_PASSWORD: 'directus'
WEBSOCKETS_ENABLED: 'true'
db:
image: postgres:16
volumes:
- directus-db:/var/lib/postgresql/data
environment:
POSTGRES_DB: directus
POSTGRES_USER: directus
POSTGRES_PASSWORD: directus
volumes:
directus-db:
directus-uploads:
directus-extensions:
Environment variables
SECRET=your-secret-key-min-256-bits
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=your-admin-password
DB_CLIENT=pg
DB_HOST=localhost
DB_PORT=5432
DB_DATABASE=directus
DB_USER=directus
DB_PASSWORD=directus
PUBLIC_URL=https://cms.example.com
CORS_ENABLED=true
CORS_ORIGIN=https://example.com
WEBSOCKETS_ENABLED=true
Key Techniques
TypeScript SDK setup
// lib/directus.ts
import { createDirectus, rest, graphql, authentication,
readItems, readItem, createItem, updateItem, deleteItem,
readSingleton, uploadFiles } from '@directus/sdk';
interface Post {
id: number;
title: string;
slug: string;
body: string;
featured_image: string | DirectusFile;
author: number | Author;
status: 'draft' | 'published' | 'archived';
date_published: string;
tags: string[];
}
interface Author {
id: number;
name: string;
bio: string;
avatar: string | DirectusFile;
}
interface DirectusFile {
id: string;
filename_download: string;
width: number;
height: number;
}
interface Settings {
site_name: string;
tagline: string;
logo: string;
}
interface Schema {
posts: Post[];
authors: Author[];
settings: Settings;
directus_files: DirectusFile[];
}
const directus = createDirectus<Schema>(
process.env.DIRECTUS_URL || 'http://localhost:8055'
)
.with(rest())
.with(graphql());
export default directus;
Fetching data with the SDK
// lib/api.ts
import directus from './directus';
import { readItems, readItem } from '@directus/sdk';
export async function getPosts() {
return directus.request(
readItems('posts', {
filter: {
status: { _eq: 'published' },
date_published: { _lte: '$NOW' },
},
sort: ['-date_published'],
limit: 20,
fields: [
'id',
'title',
'slug',
'date_published',
'tags',
{ author: ['name', 'avatar'] },
{ featured_image: ['id', 'width', 'height'] },
],
})
);
}
export async function getPostBySlug(slug: string) {
const posts = await directus.request(
readItems('posts', {
filter: {
slug: { _eq: slug },
status: { _eq: 'published' },
},
fields: [
'*',
{ author: ['*'] },
{ featured_image: ['id', 'width', 'height', 'filename_download'] },
],
limit: 1,
})
);
return posts[0] ?? null;
}
export async function getSettings() {
return directus.request(readSingleton('settings'));
}
Asset URLs and transformations
// lib/assets.ts
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'http://localhost:8055';
export function getAssetUrl(
fileId: string,
transforms?: {
width?: number;
height?: number;
fit?: 'cover' | 'contain' | 'inside' | 'outside';
quality?: number;
format?: 'jpg' | 'png' | 'webp' | 'avif';
}
): string {
const url = new URL(`/assets/${fileId}`, DIRECTUS_URL);
if (transforms) {
if (transforms.width) url.searchParams.set('width', String(transforms.width));
if (transforms.height) url.searchParams.set('height', String(transforms.height));
if (transforms.fit) url.searchParams.set('fit', transforms.fit);
if (transforms.quality) url.searchParams.set('quality', String(transforms.quality));
if (transforms.format) url.searchParams.set('format', transforms.format);
}
return url.toString();
}
// Usage
// getAssetUrl(post.featured_image.id, { width: 800, format: 'webp', quality: 80 })
GraphQL queries
// lib/graphql.ts
import directus from './directus';
export async function getPostsGraphQL() {
return directus.query<{
posts: Post[];
}>(`
query {
posts(
filter: { status: { _eq: "published" } }
sort: ["-date_published"]
limit: 20
) {
id
title
slug
date_published
tags
author {
name
avatar { id }
}
featured_image {
id
width
height
}
}
}
`);
}
Flows and automations
// Extensions: custom operation for a Flow
// extensions/operations/send-notification/index.ts
import { defineOperationApi } from '@directus/extensions-sdk';
export default defineOperationApi({
id: 'send-notification',
handler: async ({ message, channel }, { services, getSchema }) => {
const { ItemsService } = services;
const schema = await getSchema();
const notificationsService = new ItemsService('notifications', {
schema,
accountability: { admin: true },
});
await notificationsService.createOne({
message,
channel,
created_at: new Date().toISOString(),
});
return { sent: true };
},
});
Real-time subscriptions
// lib/realtime.ts
import { createDirectus, realtime } from '@directus/sdk';
const realtimeClient = createDirectus<Schema>(
process.env.DIRECTUS_URL || 'http://localhost:8055'
).with(realtime());
export async function subscribeToChanges(collection: string) {
const { subscription } = await realtimeClient.subscribe(collection, {
event: 'update',
query: {
fields: ['id', 'title', 'status'],
},
});
for await (const event of subscription) {
console.log('Change detected:', event);
// Trigger revalidation, update UI, etc.
}
}
Next.js integration with on-demand revalidation
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
export async function POST(req: Request) {
const secret = req.headers.get('x-webhook-secret');
if (secret !== process.env.DIRECTUS_WEBHOOK_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
const body = await req.json();
const collection = body.collection;
// Revalidate based on collection
if (collection === 'posts') {
revalidateTag('posts');
}
return Response.json({ revalidated: true });
}
// Fetching with cache tags
// app/blog/page.tsx
import { getPosts } from '@/lib/api';
export default async function BlogPage() {
const posts = await getPosts();
// Configure caching via fetch options in the SDK
// or wrap with unstable_cache
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
);
}
Roles and permissions
// Configure via API
import { createDirectus, rest, createRole, createPermission } from '@directus/sdk';
const admin = createDirectus(process.env.DIRECTUS_URL!)
.with(rest())
.with(authentication());
// Create a role
const editorRole = await admin.request(
createRole({
name: 'Editor',
description: 'Can manage posts but not settings',
admin_access: false,
app_access: true,
})
);
// Set permissions for the role
await admin.request(
createPermission({
role: editorRole.id,
collection: 'posts',
action: 'read',
fields: ['*'],
permissions: {}, // no row-level filter = read all
})
);
await admin.request(
createPermission({
role: editorRole.id,
collection: 'posts',
action: 'update',
fields: ['title', 'body', 'featured_image', 'tags', 'status'],
permissions: {
author: { _eq: '$CURRENT_USER' }, // can only edit own posts
},
})
);
Custom extensions
// extensions/endpoints/custom-search/index.ts
import { defineEndpoint } from '@directus/extensions-sdk';
export default defineEndpoint((router, { services, getSchema }) => {
router.get('/search', async (req, res) => {
const { ItemsService } = services;
const schema = await getSchema();
const query = req.query.q as string;
const postsService = new ItemsService('posts', {
schema,
accountability: req.accountability,
});
const results = await postsService.readByQuery({
filter: {
_or: [
{ title: { _contains: query } },
{ body: { _contains: query } },
{ tags: { _contains: query } },
],
},
fields: ['id', 'title', 'slug', 'date_published'],
limit: 20,
});
res.json({ data: results });
});
});
Best Practices
- Use the TypeScript SDK with a typed schema interface for full type safety
- Use
fieldsparameter to select only needed columns — avoid fetching*in production - Use
filterwith Directus operators (_eq,_contains,_in,_gte) for efficient queries - Use asset transformations via URL parameters — Directus handles resizing and format conversion
- Use Flows for automation (email on publish, image processing, sync to external services)
- Use roles and row-level permissions to restrict data access without custom middleware
- Use webhooks or Flows to trigger Next.js revalidation when content changes
- Use Docker volumes for uploads and database data to persist across container restarts
Anti-Patterns
- Fetching all fields with
*in list views — select only the fields you need for performance - Not configuring CORS — frontend requests will be blocked in production
- Using the admin token in client-side code — it has full database access; use a public role
- Ignoring row-level permissions — granting collection-level read without filters exposes all rows
- Running without
SECRETset — defaults are insecure and tokens can be forged - Storing Directus config in the database without backup — schema snapshots should be in Git
- Not using Docker health checks — Directus needs the database to be ready before it starts
- Skipping schema snapshots (
npx directus schema snapshot) — makes environment sync impossible
Install this skill directly: skilldb add cms-services-skills
Related Skills
Ghost
Integrate Ghost as a powerful headless CMS or a full-featured publishing platform. Use this skill when you need a content-focused solution for blogs, newsletters, membership sites, or any web application requiring robust content management and a streamlined editorial experience.
Hygraph
Build with Hygraph (formerly GraphCMS) as a GraphQL-native headless CMS. Use this skill when the project needs a CMS with a native GraphQL API, content federation, localization, scheduled publishing, asset management, or integration with Next.js, Nuxt, or Astro. Covers schema modeling, querying with GraphQL, mutations, webhooks, and caching strategies.
Keystatic
Build with Keystatic for git-based content management. Use this skill when the project needs a CMS that stores content in markdown, MDX, JSON, or YAML files committed to Git. Covers local mode, GitHub mode, content collections, singletons, component blocks, and integration with Next.js and Astro.
Notion As Cms
Leverage Notion as a flexible, collaborative content management system for your web applications. Use this skill when you need a user-friendly editor, rich content types, and programmatic access to structured data stored in Notion databases and pages.
Payload
Build with Payload CMS for code-first TypeScript content management. Use this skill when the project needs Payload integration for collections, fields, hooks, access control, local API, admin panel, or Next.js native embedding. Covers Payload 3.x with its Next.js-native architecture.
Prismic
Prismic is a headless CMS focused on content modeling and multi-channel delivery. Use this skill when projects require flexible content structures, rich text editing, internationalization, and a robust GraphQL/REST API for publishing digital experiences across various front-ends.