Configuración Avanzada de Directus Headless CMS

Directus es un headless CMS y plataforma de datos de código abierto que convierte cualquier base de datos SQL en una API REST y GraphQL con panel de administración. Sus características avanzadas como Flows para automatización, permisos granulares por campo, extensiones personalizadas y soporte para múltiples bases de datos lo convierten en una solución enterprise accesible. Esta guía cubre las configuraciones avanzadas más importantes de Directus en entornos de producción Linux.

Requisitos Previos

  • Docker y Docker Compose instalados
  • PostgreSQL, MySQL o SQLite (Directus soporta todos)
  • 1 GB RAM mínimo (recomendado 2 GB)
  • Node.js 18+ (solo para desarrollo de extensiones)

Instalación con Docker

# Crear estructura de directorios
mkdir -p /opt/directus/{uploads,extensions,database}
cd /opt/directus

# docker-compose.yml para Directus con PostgreSQL
cat > docker-compose.yml << 'EOF'
version: '3'

services:
  database:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: directus
      POSTGRES_USER: directus
      POSTGRES_PASSWORD: password_seguro_aqui
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    restart: unless-stopped

  directus:
    image: directus/directus:latest
    ports:
      - "8055:8055"
    volumes:
      - ./uploads:/directus/uploads
      - ./extensions:/directus/extensions
    depends_on:
      - database
      - cache
    environment:
      SECRET: secreto-muy-largo-y-aleatorio-de-produccion
      
      # Base de datos
      DB_CLIENT: pg
      DB_HOST: database
      DB_PORT: 5432
      DB_DATABASE: directus
      DB_USER: directus
      DB_PASSWORD: password_seguro_aqui
      
      # Caché con Redis
      CACHE_ENABLED: "true"
      CACHE_STORE: redis
      REDIS: redis://cache:6379
      
      # Admin inicial
      ADMIN_EMAIL: [email protected]
      ADMIN_PASSWORD: AdminPassword123!
      
      # URL pública
      PUBLIC_URL: https://cms.tudominio.com
      
      # CORS
      CORS_ENABLED: "true"
      CORS_ORIGIN: "https://tuapp.com"
      
      # Email
      EMAIL_FROM: [email protected]
      EMAIL_TRANSPORT: smtp
      EMAIL_SMTP_HOST: smtp.tudominio.com
      EMAIL_SMTP_PORT: 587
      EMAIL_SMTP_USER: [email protected]
      EMAIL_SMTP_PASSWORD: tu-smtp-password
      
      # Storage
      STORAGE_LOCATIONS: local
      STORAGE_LOCAL_DRIVER: local
      STORAGE_LOCAL_ROOT: ./uploads
    restart: unless-stopped

volumes:
  postgres_data:
EOF

# Iniciar Directus
docker compose up -d

# Ver logs del inicio
docker compose logs directus -f

El panel de administración estará disponible en http://localhost:8055.

Sistema de Permisos Granulares

Directus tiene un sistema de permisos por rol, colección y campo:

Configurar roles y permisos vía API

# Obtener token de admin
TOKEN=$(curl -s -X POST http://localhost:8055/auth/login \
    -H "Content-Type: application/json" \
    -d '{"email":"[email protected]","password":"AdminPassword123!"}' \
    | jq -r '.data.access_token')

# Crear un nuevo rol "Editor"
ROL_ID=$(curl -s -X POST http://localhost:8055/roles \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
        "name": "Editor",
        "description": "Puede editar contenido pero no configuración",
        "admin_access": false,
        "app_access": true
    }' | jq -r '.data.id')

echo "Rol Editor creado con ID: $ROL_ID"

# Asignar permisos a la colección "articulos" para el rol Editor
curl -X POST http://localhost:8055/permissions \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d "{
        \"role\": \"$ROL_ID\",
        \"collection\": \"articulos\",
        \"action\": \"read\",
        \"fields\": \"*\",
        \"filter\": {\"estado\": {\"_eq\": \"publicado\"}}
    }"

# Permiso de actualización solo en sus propios artículos
curl -X POST http://localhost:8055/permissions \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d "{
        \"role\": \"$ROL_ID\",
        \"collection\": \"articulos\",
        \"action\": \"update\",
        \"fields\": [\"titulo\", \"contenido\", \"estado\"],
        \"filter\": {\"autor\": {\"_eq\": \"\$CURRENT_USER\"}}
    }"

Permisos por campo (Field-level Security)

# Limitar qué campos puede ver un rol en la colección "usuarios"
curl -X POST http://localhost:8055/permissions \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d "{
        \"role\": \"$ROL_ID\",
        \"collection\": \"directus_users\",
        \"action\": \"read\",
        \"fields\": [\"id\", \"first_name\", \"last_name\", \"email\"],
        \"comment\": \"Los editores no pueden ver campos sensibles\"
    }"

Flows: Automatización Sin Código

Los Flows de Directus permiten crear automatizaciones visuales que responden a eventos o se ejecutan en horario:

Crear un Flow por API

# Crear un Flow que envía webhook al publicar un artículo
curl -X POST http://localhost:8055/flows \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
        "name": "Notificar publicación",
        "status": "active",
        "trigger": "event",
        "options": {
            "type": "filter",
            "scope": ["items.update"],
            "collections": ["articulos"]
        }
    }'

Tipos de triggers en Flows

  • Event Hook: Responde a operaciones en colecciones (crear, actualizar, eliminar)
  • Webhook Trigger: Expone un endpoint HTTP para disparar el flow
  • Schedule: Ejecuta el flow en horario cron
  • Manual: Se activa desde el panel de administración

Operaciones disponibles en Flows

Desde el panel visual de Flows (Settings > Flows), puedes encadenar:

  • Condition: Lógica condicional (if/else)
  • Run Script: Código JavaScript personalizado
  • Request URL: Llamada HTTP a API externa
  • Send Email: Enviar email con plantilla
  • Send Notification: Notificación en el panel
  • Create/Read/Update/Delete Item: CRUD en colecciones
  • Transform Payload: Transformar datos con JavaScript
// Ejemplo de operación "Run Script" en un Flow
// Código para calcular el tiempo de lectura de un artículo

const { contenido } = $input.body;
const palabras = contenido.split(' ').length;
const tiempoLectura = Math.ceil(palabras / 200); // 200 palabras por minuto

return {
    tiempo_lectura: tiempoLectura
};

Extensiones Personalizadas

Directus permite crear extensiones para personalizar el panel y la API:

Tipos de extensiones

  • Interface: Componente Vue.js personalizado para edición de campos
  • Display: Cómo se muestra un valor en listas
  • Panel: Widget para el dashboard de analítica
  • Hook: Código que se ejecuta en eventos del servidor
  • Endpoint: Endpoints de API personalizados
  • Operation: Nuevas operaciones para Flows

Crear una extensión de tipo Hook

# Instalar la CLI de Directus para desarrollo de extensiones
npm install -g @directus/cli

# Crear una nueva extensión
npx create-directus-extension@latest

# Seleccionar:
# - Tipo: hook
# - Lenguaje: TypeScript
# - Nombre: mi-hook-personalizado

cd mi-hook-personalizado
npm install
// src/index.ts - Hook que procesa imágenes al subir
import { defineHook } from '@directus/extensions-sdk';

export default defineHook(({ filter, action }, { services, logger }) => {
    // Ejecutar después de subir un archivo
    action('files.upload', async ({ payload, key }) => {
        logger.info(`Archivo subido: ${payload.filename_disk}`);
        
        // Lógica personalizada: actualizar metadatos
        const { FilesService } = services;
        const filesService = new FilesService({
            schema: await getSchema(),
            accountability: { admin: true }
        });
        
        await filesService.updateOne(key, {
            description: `Subido automáticamente el ${new Date().toISOString()}`
        });
    });
    
    // Validar antes de crear un artículo
    filter('articulos.items.create', async (payload) => {
        if (!payload.titulo || payload.titulo.length < 5) {
            throw new Error('El título debe tener al menos 5 caracteres');
        }
        
        // Generar slug automáticamente
        payload.slug = payload.titulo
            .toLowerCase()
            .replace(/[áàä]/g, 'a')
            .replace(/[éèë]/g, 'e')
            .replace(/\s+/g, '-')
            .replace(/[^a-z0-9-]/g, '');
        
        return payload;
    });
});
# Compilar la extensión
npm run build

# Copiar al directorio de extensiones de Directus
cp -r dist/ /opt/directus/extensions/hooks/mi-hook-personalizado/

# Reiniciar Directus para cargar la extensión
docker compose restart directus

Transformación de Activos

Directus puede transformar imágenes al vuelo mediante parámetros en la URL:

# URL base de un archivo
# http://localhost:8055/assets/UUID_DEL_ARCHIVO

# Transformaciones disponibles vía query params:
# - width, height: redimensionar
# - fit: cover, contain, fill, inside, outside
# - quality: 1-100
# - format: jpg, png, webp, avif
# - focal_point_x, focal_point_y: punto focal para recorte

# Ejemplos de transformaciones
# Thumbnail 400x300 con recorte centrado en WebP
curl "http://localhost:8055/assets/UUID?width=400&height=300&fit=cover&format=webp&quality=80" \
    -H "Authorization: Bearer $TOKEN" \
    -o imagen-transformada.webp

# Imagen redimensionada manteniendo proporción
curl "http://localhost:8055/assets/UUID?width=800&fit=contain" \
    -H "Authorization: Bearer $TOKEN"

Configurar transformaciones predefinidas

# Agregar al .env o docker-compose.yml
ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION: 4096
ASSETS_CONTENT_SECURITY_POLICY: "false"

# Configurar cache de transformaciones
ASSETS_CACHE_TTL: "30d"

Webhooks y Eventos

# Crear un webhook que se dispara al crear artículos
curl -X POST http://localhost:8055/webhooks \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
        "name": "Notificar sistema externo",
        "method": "POST",
        "url": "https://api.sistema-externo.com/webhook/directus",
        "status": "active",
        "data": true,
        "actions": ["create", "update"],
        "collections": ["articulos"],
        "headers": [
            {"header": "X-API-Key", "value": "mi-api-key-secreta"},
            {"header": "Content-Type", "value": "application/json"}
        ]
    }'

# Listar webhooks configurados
curl http://localhost:8055/webhooks \
    -H "Authorization: Bearer $TOKEN" | jq '.data[]'

Solución de Problemas

Error de conexión a la base de datos

# Verificar que PostgreSQL está corriendo
docker compose ps database

# Probar la conexión manualmente
docker exec -it directus-database-1 psql -U directus -d directus -c "\l"

# Ver los logs de Directus al iniciar
docker compose logs directus --tail=50 | grep -i "error\|database\|connection"

Extensión no se carga

# Verificar la estructura del directorio de extensiones
ls -la /opt/directus/extensions/

# La extensión debe estar en la subcarpeta correcta según su tipo:
# hooks/, endpoints/, interfaces/, displays/, panels/, operations/

# Ver los logs de carga de extensiones
docker compose logs directus | grep -i "extension"

# Verificar permisos del directorio
chown -R 1000:1000 /opt/directus/extensions/

Flows no se ejecutan

# Verificar que el Flow está activo
curl http://localhost:8055/flows \
    -H "Authorization: Bearer $TOKEN" | jq '.data[] | {name, status}'

# Ver el historial de ejecuciones de un Flow
curl "http://localhost:8055/flows/FLOW_ID" \
    -H "Authorization: Bearer $TOKEN" | jq '.data'

Permisos no funcionan como se espera

# Verificar los permisos asignados a un rol
curl "http://localhost:8055/permissions?filter[role][_eq]=ROL_ID" \
    -H "Authorization: Bearer $TOKEN" | jq '.data'

# Probar con el token de un usuario con ese rol
TOKEN_EDITOR=$(curl -s -X POST http://localhost:8055/auth/login \
    -H "Content-Type: application/json" \
    -d '{"email":"[email protected]","password":"password"}' \
    | jq -r '.data.access_token')

# Probar acceso con el token del editor
curl http://localhost:8055/items/articulos \
    -H "Authorization: Bearer $TOKEN_EDITOR"

Conclusión

Directus sobresale como headless CMS cuando los requisitos van más allá de un CMS estándar: permisos granulares por campo, automatización sin código con Flows, extensiones personalizadas y soporte multi-base-de-datos lo hacen especialmente valioso en entornos enterprise. Su capacidad para conectarse a bases de datos existentes sin modificar su estructura lo convierte en una capa de gestión que puede superponer sobre infraestructura de datos ya existente, sin migración ni riesgo de pérdida de datos.