Directus
Build with Directus for database-first content management. Use this skill
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: 465 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
Builder Io
Builder.io is a Visual Headless CMS and API that empowers developers to integrate
Caisy
caisy is a headless CMS designed for speed and scalability, empowering developers
Contentful
Build with Contentful for headless content management. Use this skill when the
Cosmic
Integrate Cosmic as your headless content management system, providing
Ghost
Integrate Ghost as a powerful headless CMS or a full-featured publishing platform.
Hygraph
Build with Hygraph (formerly GraphCMS) as a GraphQL-native headless CMS.