• Tech Support ⤴
  • Projects
  • Services
    • AI Development
    • UI/UX Design
    • Web Development
    • Technology Support
    • Mobile App Development
    • Banking ATM Interfaces
    • Process Automation
    • Security Auditing
    • Local AI Servers
  • odoo ERP
get in touchStart with Eva
logo
Tech Support ⤴
Projects
Services
AI DevelopmentUI/UX DesignWeb DevelopmentTechnology SupportMobile App DevelopmentBanking ATM InterfacesProcess AutomationSecurity AuditingLocal AI Servers
odoo ERP
get in touchStart with Eva
Loading…
logo

Transforming businesses through AI-powered digital innovation and creative excellence.

Quick Links

BlogAinexProjectsContact us

Contact Us

pinDubai Digital Park, A5, DTEC - Silicon Oasisemail[email protected]phone+971 55 7538087
© 2026 aratech. All rights reserved.
Privacy PolicyTerms of ServiceCookie Policy
Home / Blog / Vibe-Coding a Backend in 2026: How Directus and the nikola66 Skill Turn Natural Language Into Production Schemas

Vibe-Coding a Backend in 2026: How Directus and the nikola66 Skill Turn Natural Language Into Production Schemas

What if you could build an entire backend — complete with collections, relationships, and data — with a single prompt? Here's how Directus and the

May 3, 2026 - 25 min read

Key Takeaways

ExpandCollapse
  • - The Directus skill is a universal, collection-agnostic GraphQL client that lets AI agents create and manage any Directus schema without hardcoded models
  • - Vibe-coding a backend means describing your full data model in natural language, then letting the agent scaffold it all in one execution — collections, fields, relationships, and even seed data
  • - Production patterns include everything-you-need-in-one-call field definitions, admin token requirements, and the two-step blog post creation pattern (parent then translation records)
Vibe coding a whole backend with Directus

Table of Contents

  • Introduction: The Backend Is Now a Conversation
  • What Is This "Directus Skill" Anyway
  • Prerequisites: What You Need Before You Start
  • Installing the Skill in Hermes Agent
  • Anatomy of a Single-Prompt Backend Specification
  • Example 1: Blog Backend (Multilingual-Ready)
    • What This Does
    • The Generated Code (What's Running Under the Hood)
  • Example 2: E-Commerce Backend in One Prompt
  • Example 3: Simple CRM (Contacts + Companies + Interactions)
  • How It Actually Works: The Skill's Secret Sauce
    • Step 1 - Agent Parses Your Prompt
    • Step 2 - Skill Generates Correct Payloads
    • Step 3 - Admin API Calls in Order
    • Step 4 - Seed Data + Verification
  • Production-Proven Patterns We've Discovered
    • 1. Schema and Meta Are Mandatory
    • 2. Use get_expanded() for Relations
    • 3. IDs Are Strings in GraphQL, Integers in REST
    • 4. Junction Table Writes Go Through the Junction Collection
    • 5. Translation Collections Need Two-Step Inserts
    • 6. The POST Truncation Bug and the PATCH Fix
    • 7. Verify Write Access Before Publishing
  • Error Handling: What Goes Wrong and How to Fix It
  • Advanced Patterns: Filters, Relations, and Enum Fields
    • Filtering with Operators
    • Multi-Level Relation Expansion
    • JSON Fields for Flexible Data
  • The Admin API: When You Need to Touch Schema Programmatically
  • Putting It All Together: Your First Vibe-Code Session
  • What This Enables: Rapid Prototyping at Scale
  • Limitations and Gotchas
    • Not All Backend Patterns Fit Directus
    • You Still Need to Think About Data Modeling
    • Permissions Still Need Configuration
    • Large Content Requires Patching
  • Conclusion: The Backend Is Now a Conversation
  • Next Steps

Introduction: The Backend Is Now a Conversation

!Vibe-coding workflow diagram: natural language prompt to Directus schema to production API

Here's a frustrating pattern most developers know well:

  1. You have a brilliant app idea
  2. You start scaffolding the database - users, posts, orders
  3. You build the API layer - REST endpoints, GraphQL resolvers
  4. You wire up auth, permissions, validation
  5. Three days later you finally get to the actual feature you wanted to build

That friction is real. And in 2026, it's dead.

What if you could skip steps 2 through 4 entirely? What if you could describe your entire backend in plain English, hit enter, and have a production-ready schema, API, and admin panel ready to go?

That's what vibe-coding a backend means. And with Directus - the open-source headless CMS that turns your database into an instant API - paired with the nikola66 Directus skill (a universal GraphQL client for AI agents), it's now possible to scaffold an entire data layer in one shot.

This isn't a toy demo. This is a real workflow we're using at Ainex to spin up project backends in minutes, not days. In this guide, you'll learn:

  • What the Directus skill actually does under the hood
  • How to install it and connect to your Directus instance
  • The anatomy of a single-prompt backend specification
  • Step-by-step examples: building a blog, an e-commerce store, and a CRM
  • Production patterns, error handling, and gotchas we've already hit in the wild
  • How to verify your newly-scaffolded backend is live and queryable

Let's get into it.


What Is This "Directus Skill" Anyway?

The skill at nikola66/directus-skill is a Python client that speaks Directus's GraphQL and REST APIs directly. It's designed for AI agents like Hermes, but you can also use it as a standalone library.

Key properties that make it vibe-coding possible:

  • Collection-agnostic: It doesn't care if you're working with Blog_Posts or Ecom_Products. The same methods work on any collection.
  • Full CRUD surface: get, insert, update, delete, count, and even create_collection and create_field for schema operations (admin only).
  • Smart relation expansion: Use dot-notation like author.name or order_items.product.title and the skill builds the correct nested GraphQL selection sets.
  • Filter operators: All Directus filters supported - _eq, _contains, _in, _gt, _null, etc.
  • Error handling: Raises DirectusError with structured details so you know exactly why something failed.

When you load this skill into an AI agent, you give that agent the ability to bootstrap an entire database schema through conversation.


Prerequisites: What You Need Before You Start

Before you can vibe-code a backend, you need three things:

  1. A Directus instance - either self-hosted or Directus Cloud. We run at https://hub.aratech.ae.
  2. An API token with appropriate permissions. For schema creation, you'll need an admin token. Get it from Directus → Settings → API Tokens → Create (Admin scope, all permissions).
  3. Python 3.9+ with the skill installed:
# Option A: Install from PyPI (when published)
pip install directus-skill

## Option B: Clone and use directly (current)
git clone https://github.com/nikola66/directus-skill.git
## Copy the package into your project or add to sys.path

As of April 2026, the skill is actively maintained and production-validated against a Directus 10+ instance with full CRUD, filter operators, and collection management working reliably.


Installing the Skill in Hermes Agent

If you're running Hermes Agent (our multi-agent orchestration platform), adding the skill is a single command:

hermes skills install directus

Or to add the upstream GitHub version directly:

npx skills add nikola66/directus-skill

Once installed, the directus skill is loaded automatically when you ask about Directus operations. You don't need to manually import anything - the agent handles that.

Set your environment variables (we keep these in ~/.bashrc or the gateway session):

export DIRECTUS_URL="https://hub.aratech.ae"
export DIRECTUS_API_TOKEN="user_token_here"          # CRUD operations
export DIRECTUS_ADMIN_TOKEN="admin_token_here"      # Schema management

Note: In the Hermes gateway, these are injected automatically from the gateway config. If you're running standalone scripts, set them yourself.


Anatomy of a Single-Prompt Backend Specification

The magic happens when you describe your entire data model in one natural-language prompt. Here's the pattern:

Build me a [TYPE] backend with Directus. Create these collections:

1. [Collection Name] - [purpose]
   - field_name (type), field2 (type), ...
   - relationships: [relation details]

2. [Collection Name] - ...
   ...

After creating all collections:
- Verify by calling get_collections() and printing the list
- Seed [N] sample records for each collection
- Output the complete Python script

The agent interprets this, translates it into Directus admin API calls (POST /collections with full field definitions), executes it, and returns a working schema.

The skill does the heavy lifting:

  1. Builds the correct payload structure for Directus's quirky schema and meta requirements
  2. Calls create_collection() per collection with the full field list
  3. Optionally seeds sample data via CRUD operations
  4. Verifies success with get_collections()

All in one execution, no manual intervention.


Example 1: Blog Backend (Multilingual-Ready)

Let's start with a classic - a blog that supports 5 languages. Here's the complete prompt you'd give the agent:

Build me a multilingual blog backend with Directus. Create these collections:

1. Blog_Authors - author profiles
   - name (string), slug (string, unique), bio (text), avatar (file), social_twitter (string, nullable)
   - status: draft/published
   - Fields: id, created_at, updated_at

2. Blog_Categories - hierarchical taxonomy
   - name (string), slug (string, unique), description (text), parent_category (many-to-one → Blog_Categories, nullable)
   - icon (string, icon name), color (string, hex)
   - Fields: id, sort_order

3. Blog_Tags - flat tags
   - name (string), slug (string, unique), color (string, hex)
   - Fields: id

4. Blog_Posts - parent records (metadata only, per our schema design)
   - status (draft/published), published_at (datetime, nullable)
   - author (many-to-one → Blog_Authors), category (many-to-one → Blog_Categories)
   - featured_image (file), reading_time_minutes (integer)
   - is_featured (boolean), is_pillar (boolean)
   - Fields: id, created_at, updated_at

5. Blog_Posts_Translations - per-language content (child records)
   - blog_posts_id (FK integer → Blog_Posts), languages_code (string: en/ar/de/es/fr)
   - title (string), slug (string), content (markdown text)
   - excerpt (text, max 200 chars), key_takeaways (json array, 3-5 items)
   - meta_title (string, ≤60), meta_description (string, ≤160)
   - featured_image_alt (string)
   - cta_headline (string), cta_button_text (string), cta_button_url (string)
   - schema_markup (json - Article schema.org)
   - Fields: id

6. Blog_Posts_Tags - junction for many-to-many post ↔ tag
   - blog_posts_id (FK → Blog_Posts), blog_tags_id (FK → Blog_Tags)
   - Fields: id

Important:
- Use create_collection() for all 6 collections
- Every field must include schema: {} and meta: {} in the payload
- Define ALL fields up front (no incremental field adds)
- After creating collections, seed 2 sample blog posts minimum:
  * Post 1: "Introduction to AI Security" - author=1, category=33 (AI & LLMs), published
  * Post 2: "Understanding SOC 2 Compliance" - author=1, category=37 (Compliance), draft
  * For each post, create EN translation only (skip AR/DE/ES/FR for now)
- Print verification: list all collections and their record counts

What This Does

When you run this prompt through an agent with the Directus skill loaded:

  1. Creates 6 collections with full field definitions in correct order (parents first, then junctions, then translations)
  2. Adds proper relationships - Many-to-One from Posts → Authors/Category, Many-to-Many via junction table
  3. Respects your actual schema design - Translation records are separate child collection linked by blog_posts_id
  4. Seeds real data - Two sample posts with required fields filled
  5. Verifies - Prints what was created

The Generated Code (What's Running Under the Hood)

The agent produces something like this Python script:

from directus.client import DirectusClient
import os

client = DirectusClient(
    url=os.getenv('DIRECTUS_URL'),
    token=os.getenv('DIRECTUS_ADMIN_TOKEN')  # admin token for schema ops
)

## 1. Blog_Authors
client.create_collection('Blog_Authors', fields=[
    {'field': 'id', 'type': 'integer', 'schema': {'is_primary_key': True, 'has_auto_increment': True}, 'meta': {'hidden': True, 'interface': 'numeric', 'readonly': True}},
    {'field': 'name', 'type': 'string', 'schema': {}, 'meta': {'interface': 'input'}},
    {'field': 'slug', 'type': 'string', 'schema': {'is_unique': True}, 'meta': {'interface': 'input'}},
    {'field': 'bio', 'type': 'text', 'schema': {}, 'meta': {'interface': 'textarea'}},
    {'field': 'avatar', 'type': 'file', 'schema': {}, 'meta': {'interface': 'file', 'options': {'accepted_files': 'image/*'}}},
    {'field': 'social_twitter', 'type': 'string', 'schema': {}, 'meta': {'interface': 'input'}},
    {'field': 'status', 'type': 'string', 'schema': {}, 'meta': {'interface': 'select', 'options': {'choices': ['draft', 'published']}}},
    {'field': 'created_at', 'type': 'timestamp', 'schema': {}, 'meta': {'interface': 'datetime', 'readonly': True}},
    {'field': 'updated_at', 'type': 'timestamp', 'schema': {}, 'meta': {'interface': 'datetime', 'readonly': True}}
])

## 2. Blog_Categories
client.create_collection('Blog_Categories', fields=[
    {'field': 'id', 'type': 'integer', 'schema': {'is_primary_key': True, 'has_auto_increment': True}, 'meta': {'hidden': True, 'interface': 'numeric', 'readonly': True}},
    {'field': 'name', 'type': 'string', 'schema': {}, 'meta': {'interface': 'input'}},
    {'field': 'slug', 'type': 'string', 'schema': {'is_unique': True}, 'meta': {'interface': 'input'}},
    {'field': 'description', 'type': 'text', 'schema': {}, 'meta': {'interface': 'textarea'}},
    {'field': 'parent_category', 'type': 'integer', 'schema': {}, 'meta': {'interface': 'relation', 'options': {'related_collection': 'Blog_Categories'}}},
    {'field': 'icon', 'type': 'string', 'schema': {}, 'meta': {'interface': 'input'}},
    {'field': 'color', 'type': 'string', 'schema': {}, 'meta': {'interface': 'color'}},
    {'field': 'sort_order', 'type': 'integer', 'schema': {}, 'meta': {'interface': 'numeric'}},
    {'field': 'created_at', 'type': 'timestamp', 'schema': {}, 'meta': {'interface': 'datetime', 'readonly': True}},
    {'field': 'updated_at', 'type': 'timestamp', 'schema': {}, 'meta': {'interface': 'datetime', 'readonly': True}}
])

## 3. Blog_Tags
client.create_collection('Blog_Tags', fields=[
    {'field': 'id', 'type': 'integer', 'schema': {'is_primary_key': True, 'has_auto_increment': True}, 'meta': {'hidden': True, 'interface': 'numeric', 'readonly': True}},
    {'field': 'name', 'type': 'string', 'schema': {}, 'meta': {'interface': 'input'}},
    {'field': 'slug', 'type': 'string', 'schema': {'is_unique': True}, 'meta': {'interface': 'input'}},
    {'field': 'color', 'type': 'string', 'schema': {}, 'meta': {'interface': 'color'}},
    {'field': 'created_at', 'type': 'timestamp', 'schema': {}, 'meta': {'interface': 'datetime', 'readonly': True}}
])

## 4. Blog_Posts - parent metadata record
client.create_collection('Blog_Posts', fields=[
    {'field': 'id', 'type': 'integer', 'schema': {'is_primary_key': True, 'has_auto_increment': True}, 'meta': {'hidden': True, 'interface': 'numeric', 'readonly': True}},
    {'field': 'status', 'type': 'string', 'schema': {}, 'meta': {'interface': 'select', 'options': {'choices': ['draft', 'published']}}},
    {'field': 'published_at', 'type': 'timestamp', 'schema': {}, 'meta': {'interface': 'datetime', 'readonly': False}},
    {'field': 'author', 'type': 'integer', 'schema': {}, 'meta': {'interface': 'relation', 'options': {'related_collection': 'Blog_Authors'}}},
    {'field': 'category', 'type': 'integer', 'schema': {}, 'meta': {'interface': 'relation', 'options': {'related_collection': 'Blog_Categories'}}},
    {'field': 'featured_image', 'type': 'file', 'schema': {}, 'meta': {'interface': 'file', 'options': {'accepted_files': 'image/*'}}},
    {'field': 'reading_time_minutes', 'type': 'integer', 'schema': {}, 'meta': {'interface': 'numeric'}},
    {'field': 'is_featured', 'type': 'boolean', 'schema': {}, 'meta': {'interface': 'boolean'}},
    {'field': 'is_pillar', 'type': 'boolean', 'schema': {}, 'meta': {'interface': 'boolean'}},
    {'field': 'created_at', 'type': 'timestamp', 'schema': {}, 'meta': {'interface': 'datetime', 'readonly': True}},
    {'field': 'updated_at', 'type': 'timestamp', 'schema': {}, 'meta': {'interface': 'datetime', 'readonly': True}}
])

## 5. Blog_Posts_Translations - content per language
client.create_collection('Blog_Posts_Translations', fields=[
    {'field': 'id', 'type': 'integer', 'schema': {'is_primary_key': True, 'has_auto_increment': True}, 'meta': {'hidden': True, 'interface': 'numeric', 'readonly': True}},
    {'field': 'blog_posts_id', 'type': 'integer', 'schema': {}, 'meta': {'interface': 'relation', 'options': {'related_collection': 'Blog_Posts'}}},
    {'field': 'languages_code', 'type': 'string', 'schema': {}, 'meta': {'interface': 'select', 'options': {'choices': ['en', 'ar', 'de', 'es', 'fr']}}},
    {'field': 'title', 'type': 'string', 'schema': {'is_required': True}, 'meta': {'interface': 'input'}},
    {'field': 'slug', 'type': 'string', 'schema': {'is_required': True, 'is_unique': True}, 'meta': {'interface': 'input'}},
    {'field': 'content', 'type': 'text', 'schema': {}, 'meta': {'interface': 'markdown'}},
    {'field': 'excerpt', 'type': 'text', 'schema': {}, 'meta': {'interface': 'textarea'}},
    {'field': 'key_takeaways', 'type': 'json', 'schema': {}, 'meta': {'interface': 'list'}},
    {'field': 'meta_title', 'type': 'string', 'schema': {}, 'meta': {'interface': 'input'}},
    {'field': 'meta_description', 'type': 'text', 'schema': {}, 'meta': {'interface': 'textarea'}},
    {'field': 'featured_image_alt', 'type': 'text', 'schema': {}, 'meta': {'interface': 'textarea'}},
    {'field': 'cta_headline', 'type': 'string', 'schema': {}, 'meta': {'interface': 'input'}},
    {'field': 'cta_button_text', 'type': 'string', 'schema': {}, 'meta': {'interface': 'input'}},
    {'field': 'cta_button_url', 'type': 'string', 'schema': {}, 'meta': {'interface': 'input'}},
    {'field': 'schema_markup', 'type': 'json', 'schema': {}, 'meta': {'interface': 'json'}},
    {'field': 'created_at', 'type': 'timestamp', 'schema': {}, 'meta': {'interface': 'datetime', 'readonly': True}},
    {'field': 'updated_at', 'type': 'timestamp', 'schema': {}, 'meta': {'interface': 'datetime', 'readonly': True}}
])

## 6. Blog_Posts_Tags - junction table
client.create_collection('Blog_Posts_Tags', fields=[
    {'field': 'id', 'type': 'integer', 'schema': {'is_primary_key': True, 'has_auto_increment': True}, 'meta': {'hidden': True, 'interface': 'numeric', 'readonly': True}},
    {'field': 'blog_posts_id', 'type': 'integer', 'schema': {}, 'meta': {'interface': 'relation', 'options': {'related_collection': 'Blog_Posts'}}},
    {'field': 'blog_tags_id', 'type': 'integer', 'schema': {}, 'meta': {'interface': 'relation', 'options': {'related_collection': 'Blog_Tags'}}},
    {'field': 'created_at', 'type': 'timestamp', 'schema': {}, 'meta': {'interface': 'datetime', 'readonly': True}}
])

## Seed sample data
## 1. Create author (if not exists)
author = client.insert({'name': 'Alex Chen', 'slug': 'alex-chen', 'bio': 'Security engineer at Ainex.', 'status': 'published'}, collection='Blog_Authors')
author_id = author['id']

## 2. Create category
category = client.insert({'name': 'AI & LLMs', 'slug': 'ai-llms', 'description': 'Articles about AI security and LLMs.', 'sort_order': 1}, collection='Blog_Categories')
category_id = category['id']

## 3. Create parent blog post record
post = client.insert({
    'status': 'published',
    'author': author_id,
    'category': category_id,
    'reading_time_minutes': 8,
    'is_featured': False,
    'published_at': '2026-05-03T10:00:00.000Z'
}, collection='Blog_Posts')
post_id = post['id']

## 4. Create English translation
client.insert({
    'blog_posts_id': post_id,
    'languages_code': 'en',
    'title': 'Introduction to AI Security',
    'slug': 'intro-to-ai-security',
    'content': '# AI Security Fundamentals\n\nThis is a sample article about securing AI systems...\n\n## Key Risks\n\n- Prompt injection\n- Data leakage\n- Model poisoning\n\n## Best Practices\n\nAlways validate inputs, sandbox model execution, and monitor for anomalies.',
    'excerpt': 'A beginner-friendly guide to understanding and mitigating AI system risks.',
    'key_takeaways': ['AI systems are vulnerable to prompt injection attacks', 'Always validate and sanitize model inputs', 'Monitor for anomalous model behavior'],
    'meta_title': 'Introduction to AI Security - Ainex Guide',
    'meta_description': 'Learn the fundamentals of AI security, common threats, and best practices for protecting your LLM applications.',
    'featured_image_alt': 'Abstract visualization of neural network security',
    'cta_headline': 'Ready to secure your AI?',
    'cta_button_text': 'Get Started',
    'cta_button_url': '/get-started',
    'schema_markup': {
        '@context': 'https://schema.org',
        '@type': 'Article',
        'headline': 'Introduction to AI Security',
        'author': {'@type': 'Person', 'name': 'Alex Chen'},
        'datePublished': '2026-05-03T10:00:00.000Z',
        'description': 'A beginner-friendly guide to understanding and mitigating AI system risks.'
    }
}, collection='Blog_Posts_Translations')

print(f"✅ Blog backend created! Post ID: {post_id}")

That script is generated and executed by the agent in a single turn. You get a full, queryable schema and seeded data immediately.


Example 2: E-Commerce Backend in One Prompt

Let's go bigger. Here's how you'd scaffold a complete e-commerce data layer:

Build a production e-commerce backend with Directus. Create these collections:

1. **Products** - catalog items
   - title, slug (unique), description (text), status (draft/available/out_of_stock)
   - price_cents (integer), compare_price_cents (integer nullable), cost_cents (integer)
   - sku (unique), barcode (nullable), weight_grams (integer)
   - inventory_quantity, low_stock_threshold (default 10)
   - featured_image (file), images (M2M via junction)
   - category (M2O → Categories), tags (M2M → Tags via junction)
   - published_at

2. **Categories** - nested taxonomy
   - name, slug (unique), description, parent_category (nullable M2O self)
   - image (file), sort_order

3. **Tags** - product tags
   - name, slug (unique), color (hex)

4. **Customers** - accounts
   - email (unique), first_name, last_name, phone
   - default_shipping_address (json), default_billing_address (json)
   - total_spent_cents, order_count, last_order_at (nullable)
   - status (active/blocked), notes

5. **Orders** - transactions
   - order_number (unique), status (pending/processing/shipped/delivered/cancelled)
   - customer (M2O → Customers)
   - subtotal_cents, tax_cents, shipping_cents, total_cents, discount_cents
   - shipping_address, billing_address (json)
   - payment_status (paid/refunded/failed), payment_method (stripe/paypal/manual)
   - payment_intent_id (nullable), tracking_number (nullable)
   - notes, cancelled_at (nullable)

6. **Order_Items** - line items (junction table Orders ↔ Products)
   - order (M2O → Orders), product (M2O → Products)
   - quantity, unit_price_cents, total_price_cents
   - variant_name (string, nullable)

7. **Inventory_Adjustments** - audit log
   - product (M2O → Products), reason (restock/sale/return/audit/adjustment)
   - delta_quantity, previous_quantity, new_quantity
   - reference_id (nullable - links to order/adjustment), notes

8. **Collections** - curated groupings
   - name, slug (unique), handle (API-friendly string)
   - type (manual/automatic), rules (json for automatic rules)
   - sort_order, published (boolean)

9. **Discounts** - promo codes
   - code (unique, nullable for automatic), type (percentage/fixed)
   - value (int - percent or cents), max_uses, used_count
   - valid_from, valid_until (nullable)
   - applies_to (all/categories/products), applicable_product_ids (json array)
   - min_order_amount_cents (nullable), exclude_sale_items (boolean)

Constraints:
- Use create_collection() for all 9 collections + explicit junction tables (Products_Images if needed)
- Every field: include schema: {} and meta: {} up front
- Use proper Directus field types: integer, string, text, boolean, datetime, json, file, pivot
- Add unique indexes on slug, sku, order_number
- Seed: create 3 sample products with sample images (use file IDs if available), link to category
- Verify: call get_collections() and print record counts
- Output the full Python script

Running this through the agent produces a ~300-line script that builds your entire e-commerce data model in one go. From zero to "here's your admin panel" before your coffee gets cold.


Example 3: Simple CRM (Contacts + Companies + Interactions)

Need something lightweight? Here's a CRM:

Build a CRM backend with Directus:

1. Companies
   - name, domain (unique), industry, size (1-10/11-50/51-200/200+)
   - website, phone, address (json), annual_revenue_cents (nullable)
   - owner (M2O → Users), status (lead/customer/prospect/churned)
   - last_contacted_at, notes (text)

2. Contacts
   - first_name, last_name, email (unique), phone, job_title
   - company (M2O → Companies), owner (M2O → Users)
   - status (active/inactive/lead), preferred_contact (email/phone/linkedin)
   - linkedin_url, notes

3. Interactions
   - contact (M2O → Contacts), type (call/email/meeting/note)
   - direction (inbound/outbound), duration_minutes (integer)
   - summary (text), next_steps (text), due_date (nullable)
   - related_deal (M2O → Deals nullable)

4. Deals
   - title, value_cents, stage (proposal/negotiation/closed_won/closed_lost)
   - probability (0-100), close_date (expected)
   - contact (M2O → Contacts), owner (M2O → Users)
   - notes

Constraints:
- Use create_collection() for all 4
- All standard fields: id, created_at, updated_at automatically added
- Relationships set up correctly (FKs are integers)
- Seed: 2 companies, 4 contacts, 3 interactions, 1 deal

Again - one prompt, one execution.


How It Actually Works: The Skill's Secret Sauce

Let's demystify what happens when you ask the agent to "build a backend":

Step 1 - Agent Parses Your Prompt

The agent uses its reasoning to extract:

  • Collection names and their purpose
  • Field names, types, and constraints
  • Relationship directions (many-to-one, many-to-many)
  • Seed data requirements
  • Verification steps

It doesn't need to see a schema definition language - it just reads your plain English and builds the structure.

Step 2 - Skill Generates Correct Payloads

This is where the skill shines. Directus's POST /collections endpoint is famously picky:

Without schema: {} and meta: {} per field? → 403 Forbidden (even with a valid admin token). Wrong field type name? → The collection silently doesn't appear. Missing relationship meta options? → Relations don't work.

The skill knows the Directus API contract and generates production-correct payloads automatically. Here's what a correct field definition looks like:

{
    'field': 'price_cents',
    'type': 'integer',
    'schema': {'is_required': True},      # ← always include this
    'meta': {'interface': 'numeric'}      # ← always include this
}

Step 3 - Admin API Calls in Order

Collections are created in dependency order (parents before children, no circular deps). For each:

  1. POST /collections creates the collection with full field list
  2. Skill verifies with GET /collections/{name} that it exists
  3. If it already exists, it drops and recreates (idempotent mode)

For junction tables (M2M), the pivot type or explicit FK fields with meta.interface: 'relation' are used.

Step 4 - Seed Data + Verification

After all collections exist, the agent:

  • Inserts sample records using standard CRUD (insert())
  • Queries back with get() or get_expanded() to confirm
  • Prints a summary to your terminal

Production-Proven Patterns We've Discovered

We've run this workflow against https://hub.aratech.ae with our full blog schema. Here are the lessons:

1. Schema and Meta Are Mandatory

This can't be stressed enough. Directus 10+ returns 403 when either is missing, but the error message is misleading ("You don't have permission") when the real issue is your payload structure.

Always check:

  • Collection payload has schema: {} and meta: {} at the top level
  • Every single field has both schema and meta
  • Field type values are valid Directus types (integer, not int; string, not varchar)

2. Use get_expanded() for Relations

Directus GraphQL requires nested selection sets for relation fields. If you try:

{ Blog_Posts_Translations { id blog_posts_id } }

You get a 400 error: Field "blog_posts_id" of type "Blog_Posts" must have a selection of subfields.

The skill's get_expanded() method with dot-notation handles this automatically:

client.get_expanded(
    fields=['id', 'title', 'blog_posts_id.id', 'blog_posts_id.status'],
    collection='Blog_Posts_Translations',
    limit=50
)
## Returns dicts: blog_posts_id = {'id': '5', 'status': 'published'}

3. IDs Are Strings in GraphQL, Integers in REST

GraphQL returns all id fields as strings. But foreign key fields (like blog_posts_id) are integers when used as filters or insert values.

When you create a post and get back post_id = "42", you must cast:

client.insert({
    'blog_posts_id': int(post_id),  # NOT post_id
    'languages_code': 'en',
    ...
}, collection='Blog_Posts_Translations')

Otherwise you get a 400 GraphQL validation error.

4. Junction Table Writes Go Through the Junction Collection

Directus exposes M2M relationships as read-only virtual fields on the parent. You cannot PATCH the tags field on Blog_Posts directly - you get 403 Forbidden.

Instead, write to the junction table:

## WRONG: Direct PATCH on Blog_Posts fails
client.update(post_id, {'tags': [1, 2, 3]}, collection='Blog_Posts')  # ❌ 403

## CORRECT: Insert into junction table
for tag_id in [1, 2, 3]:
    client.insert({'blog_posts_id': post_id, 'blog_tags_id': tag_id}, collection='Blog_Posts_Tags')

5. Translation Collections Need Two-Step Inserts

Per our schema design: first create the parent Blog_Posts record, then create the child Blog_Posts_Translations record with the FK:

## Step 1: parent (metadata only)
post = client.insert({
    'status': 'published',
    'author': author_id,
    'category': category_id,
    'reading_time_minutes': 8
}, collection='Blog_Posts')

post_id = post['id']

## Step 2: translation (actual content)
client.insert({
    'blog_posts_id': post_id,
    'languages_code': 'en',
    'title': 'My Post',
    'slug': 'my-post',
    'content': '# Hello world...',
    ...
}, collection='Blog_Posts_Translations')

6. The POST Truncation Bug and the PATCH Fix

We discovered that large content field POSTs (>~1500 chars) get silently truncated by a gateway/proxy layer. The REST response returns success, but only the first ~1.5KB of your markdown is stored.

Mitigation: Create the translation record with initial data, then immediately PATCH the content field:

## Step A: Create (truncation risk)
trans = client.insert({ ... initial_fields ..., 'content': 'short placeholder' }, collection='Blog_Posts_Translations')
trans_id = trans['id']

## Step B: PATCH full content (bypasses truncation)
client.update(trans_id, {'content': full_markdown_body}, collection='Blog_Posts_Translations')

7. Verify Write Access Before Publishing

Before attempting a critical write (publish, bulk update), do a lightweight test:

## Send a minimal test record
test = {'status': 'draft', 'author': 1, 'category': 33}
test_record = client.insert(test, collection='Blog_Posts')
test_id = test_record['id']

## If successful, delete it
client.delete(test_id, hard=True, collection='Blog_Posts')
print('✅ Write access confirmed')

If this fails with 403/404, your token doesn't have write permissions - no point attempting the full operation.


Error Handling: What Goes Wrong and How to Fix It

Here's the error table from production:

SymptomCauseFix
DirectusError: 403 Forbidden on create_collectionMissing schema or meta in field payloadAdd both to EVERY field definition
DirectusError: 400 GraphQL validation error on get()Using relation field without subfield selectionUse get_expanded() with dot-notation or include nested curly braces in field list
TypeError: id must be int, got strPassing GraphQL ID string as FK valueCast with int(record['id']) before using as FK
Silent content truncation after insertGateway POST size limit (~1.5KB)PATCH full content after initial create
create_field() corrupts GraphQL schemaIncremental field additions break schemaDefine all fields up front in create_collection()
get_collection_schema() returns empty or unexpectedField name differs from assumption (e.g. cta_button vs cta_button_text)Always fetch /fields/{collection} endpoint to get exact field list
Empty response raises JSONDecodeErrorDELETE endpoints return 204 No ContentGuard: data = json.loads(raw) if raw.strip() else {}

The skill already handles most of these internally now (v1.4.1+), but it's good to know the patterns.


Advanced Patterns: Filters, Relations, and Enum Fields

Filtering with Operators

The skill supports all Directus filter operators:

## Equal
results = client.get(
    fields=['id', 'title', 'status'],
    filters={'status': {'_eq': 'published'}},
    collection='Blog_Posts',
    limit=10
)

## Not equal
drafts = client.get(filters={'status': {'_neq': 'published'}}, collection='Blog_Posts')

## Contains (case-insensitive)
search = client.get(
    filters={'title': {'_icontains': 'security'}},
    collection='Blog_Posts'
)

## In list
results = client.get(
    filters={'category': {'_in': [33, 37, 42]}},
    collection='Blog_Posts'
)

## Greater than
expensive = client.get(
    filters={'price_cents': {'_gt': 10000}},
    collection='Products'
)

## Null check
uncontacted = client.get(
    filters={'last_contacted_at': {'_null': True}},
    collection='Contacts'
)

## Combined (ANDed together)
results = client.get(
    filters={
        'status': {'_eq': 'published'},
        'category': {'_in': [33, 37]},
        'published_at': {'_not_null': True}
    },
    collection='Blog_Posts'
)

Multi-Level Relation Expansion

With dot-notation, you can fetch nested relations in one call:

## Fetch posts with author, category, and tags
records = client.get_expanded(
    fields=[
        'id', 'title', 'status',
        'author.id', 'author.name', 'author.email',  # M2O relation
        'category.id', 'category.name',               # M2O relation
        'tags.id', 'tags.name', 'tags.slug'           # M2M via junction
    ],
    collection='Blog_Posts',
    limit=20
)
## Result: each record has nested dicts like {'author': {'id': 1, 'name': 'Alex'}, 'tags': [{'id': 5, 'name': 'AI'}, ...]}

JSON Fields for Flexible Data

Use json type for arbitrary structured data:

client.insert({
    'shipping_address': {
        'street': '123 Tech Blvd',
        'city': 'Dubai',
        'country': 'UAE',
        'postal_code': '00000'
    },
    'metadata': {
        'utm_source': 'newsletter',
        'campaign_id': 'Q2-2026-ai-launch'
    }
}, collection='Orders')

The JSON is stored natively and queryable with filters on nested keys.


The Admin API: When You Need to Touch Schema Programmatically

The skill exposes create_collection() and create_field() through the Directus admin REST API (/collections and /fields/{collection}), not GraphQL.

Admin token required. Your DIRECTUS_API_TOKEN is for CRUD; DIRECTUS_ADMIN_TOKEN is for schema operations.

client = DirectusClient(
    url=os.getenv('DIRECTUS_URL'),
    token=os.getenv('DIRECTUS_API_TOKEN'),         # for CRUD
    admin_token=os.getenv('DIRECTUS_ADMIN_TOKEN') # for schema
)

## Create a new collection
client.create_collection('TEST_Items', fields=[
    {'field': 'id', 'type': 'integer', 'schema': {'is_primary_key': True, 'has_auto_increment': True}, 'meta': {'hidden': True, 'interface': 'numeric', 'readonly': True}},
    {'field': 'name', 'type': 'string', 'schema': {}, 'meta': {'interface': 'input'}},
    {'field': 'value', 'type': 'integer', 'schema': {}, 'meta': {'interface': 'numeric'}}
])

## Add a field to existing collection (use with caution - may corrupt GraphQL schema)
client.create_field('TEST_Items', 'description', 'text', meta={'interface': 'textarea'})

## List all collections
collections = client.get_collections()
print(collections)  # ['Blog_Posts', 'Blog_Authors', ...]

## Get collection schema
schema = client.get_collection_schema('Blog_Posts')
print(schema['fields'].keys())  # dict_keys(['id', 'status', 'author', ...])

Warning: Incrementally adding fields via create_field() can corrupt the GraphQL schema for that collection, causing all subsequent GraphQL queries to fail with 400. The safest pattern: define every field up front in create_collection().


Putting It All Together: Your First Vibe-Code Session

Here's what a complete workflow looks like:

## 1. Start your terminal (with Directus tokens in env)
$ export DIRECTUS_URL="https://hub.aratech.ae"
$ export DIRECTUS_API_TOKEN="sZk-st..."      # user token
$ export DIRECTUS_ADMIN_TOKEN="ad-..."        # admin token

## 2. Launch Hermes agent
$ hermes

## 3. Give the prompt (copy-paste from above)
You: Build me a blog backend with Directus. Create these collections: ...

## 4. Agent thinks, generates script, executes
🤖 Agent: Creating Blog_Authors... ✅
   Creating Blog_Categories... ✅
   Creating Blog_Tags... ✅
   Creating Blog_Posts... ✅
   Creating Blog_Posts_Translations... ✅
   Creating Blog_Posts_Tags... ✅
   Seeding 2 sample posts... ✅
   
   Verification:
   - Blog_Posts: 2 records
   - Blog_Posts_Translations: 2 records (en only)
   - Blog_Authors: 1 record
   - Blog_Categories: 1 record
   - Blog_Tags: 0 records
   
   ✅ Backend scaffolded successfully! Your Directus admin panel at https://hub.aratech.ae/admin now shows all collections populated.

## 5. You're done. Check the admin UI or query:
$ python3 -c "from directus.client import DirectusClient; c = DirectusClient(); print(c.get(collection='Blog_Posts', limit=3))"

That's it. From blank Directus instance to a full blog backend with relationships and sample data in under a minute.


What This Enables: Rapid Prototyping at Scale

This isn't just about saving time on schema creation (though that's huge). It's about reducing the feedback loop between idea and working prototype:

  • Week-long schema design → 5 minutes of describing what you want
  • Manual migration files → One generated, executed script
  • API spec drafting → Directus auto-generates REST and GraphQL endpoints
  • Admin panel configuration → Directus admin UI is ready immediately with your collections, fields, and permissions

You can now spin up a project-specific backend for:

  • Internal tools (inventory, ticketing, asset tracking)
  • Customer portals (order history, support tickets)
  • SaaS data models (subscriptions, tenants, usage logs)
  • MVPs for client work (describe the domain, get an instant CMS-backed backend)

And because Directus is just your database plus a smart API layer, you own the data. No vendor lock-in. Export the SQL anytime.


Limitations and Gotchas

Not All Backend Patterns Fit Directus

Directus is a CMS-first platform. It excels at:

  • Content-centric models (blogs, news, docs, catalogs)
  • Structured relational data (users, orders, products)
  • Media-heavy apps (file management built-in)

It's not ideal for:

  • High-frequency transactional systems (banking, real-time bidding)
  • Complex event-sourcing or CQRS architectures
  • Systems requiring custom SQL functions or triggers
  • Real-time websockets (though you can use its hooks and webhooks)

You Still Need to Think About Data Modeling

Vibe-coding doesn't replace understanding your domain. Garbage in, garbage out:

  • Poorly named fields → confusing API
  • Missing indexes → slow queries at scale
  • Flat structure for deep nesting → inefficient reads

The skill scaffolds based on your spec. Good specs come from good modeling.

Permissions Still Need Configuration

The skill creates collections and fields as an admin, but you still need to configure Directus permissions (Roles & Policies) to expose the right data to frontend users. The skill doesn't touch permissions yet.

Large Content Requires Patching

If you're publishing markdown articles >1500 characters, always use the POST-then-PATCH pattern to avoid truncation. The skill doesn't auto-PATCH yet - you need to handle it in your seeding script.


Conclusion: The Backend Is Now a Conversation

Building a backend used to mean writing SQL migrations, configuring an ORM, building controller routes, and wiring up an admin. Days or weeks of work.

In 2026, with Directus and the nikola66 skill, it's:

  1. Describe your schema in plain English
  2. Let the agent generate and execute the Python
  3. Your backend is live

We've used this pattern to spin up three project backends this month alone. What used to be a blocker - "we need to build the data layer first" - is now a non-issue. The bottleneck moved upstream: now we spend our time on product logic and UX, not plumbing.

The vibe-coding era is here. Your backend is ready when you are.


Next Steps

  • Install the skill: npx skills add nikola66/directus-skill
  • Connect to Directus: Set DIRECTUS_URL, DIRECTUS_API_TOKEN, DIRECTUS_ADMIN_TOKEN
  • Try the blog example above (it creates a fully queryable multilingual-ready schema)
  • Extend it: Add collections for comments, likes, subscriptions, webhook logs
  • Hook up permissions in Directus Admin → Settings → Roles & Policies

Got questions? Drop us a line at Telegram @Hermdroid or open an issue on the skill repo. Happy building.


Reading time: ~9 minutes Category: Tutorial Tags: Directus, Backend Development, AI Agents, Vibe Coding, CMS, GraphQL, Python, Database Schema, API, No-Code


Related Articles

  • HyperFrames: The Open-Source Framework That Turns HTML Into Production Video
  • Gemini CLI CVE-2025-59528: When Your AI Coding Agent Opens the Back Door
  • Headless CMS Smackdown: Strapi vs Ghost vs Directus

Table of Contents

  • ↗Table of Contents
  • ↗Introduction: The Backend Is Now a Conversation
  • ↗What Is This "Directus Skill" Anyway?
  • ↗Prerequisites: What You Need Before You Start
  • ↗Option B: Clone and use directly (current)
  • ↗Copy the package into your project or add to sys.path
  • ↗Installing the Skill in Hermes Agent
  • ↗Anatomy of a Single-Prompt Backend Specification
  • ↗Example 1: Blog Backend (Multilingual-Ready)
  • ↗What This Does
  • ↗The Generated Code (What's Running Under the Hood)
  • ↗1. Blog_Authors
  • ↗2. Blog_Categories
  • ↗3. Blog_Tags
  • ↗4. Blog_Posts - parent metadata record
  • ↗5. Blog_Posts_Translations - content per language
  • ↗6. Blog_Posts_Tags - junction table
  • ↗Seed sample data
  • ↗1. Create author (if not exists)
  • ↗2. Create category
  • ↗3. Create parent blog post record
  • ↗4. Create English translation
  • ↗Example 2: E-Commerce Backend in One Prompt
  • ↗Example 3: Simple CRM (Contacts + Companies + Interactions)
  • ↗How It Actually Works: The Skill's Secret Sauce
  • ↗Step 1 - Agent Parses Your Prompt
  • ↗Step 2 - Skill Generates Correct Payloads
  • ↗Step 3 - Admin API Calls in Order
  • ↗Step 4 - Seed Data + Verification
  • ↗Production-Proven Patterns We've Discovered
  • ↗1. Schema and Meta Are Mandatory
  • ↗2. Use get_expanded() for Relations
  • ↗Returns dicts: blog_posts_id = {'id': '5', 'status': 'published'}
  • ↗3. IDs Are Strings in GraphQL, Integers in REST
  • ↗4. Junction Table Writes Go Through the Junction Collection
  • ↗WRONG: Direct PATCH on Blog_Posts fails
  • ↗CORRECT: Insert into junction table
  • ↗5. Translation Collections Need Two-Step Inserts
  • ↗Step 1: parent (metadata only)
  • ↗Step 2: translation (actual content)
  • ↗6. The POST Truncation Bug and the PATCH Fix
  • ↗Step A: Create (truncation risk)
  • ↗Step B: PATCH full content (bypasses truncation)
  • ↗7. Verify Write Access Before Publishing
  • ↗Send a minimal test record
  • ↗If successful, delete it
  • ↗Error Handling: What Goes Wrong and How to Fix It
  • ↗Advanced Patterns: Filters, Relations, and Enum Fields
  • ↗Filtering with Operators
  • ↗Equal
  • ↗Not equal
  • ↗Contains (case-insensitive)
  • ↗In list
  • ↗Greater than
  • ↗Null check
  • ↗Combined (ANDed together)
  • ↗Multi-Level Relation Expansion
  • ↗Fetch posts with author, category, and tags
  • ↗Result: each record has nested dicts like {'author': {'id': 1, 'name': 'Alex'}, 'tags': [{'id': 5, 'name': 'AI'}, ...]}
  • ↗JSON Fields for Flexible Data
  • ↗The Admin API: When You Need to Touch Schema Programmatically
  • ↗Create a new collection
  • ↗Add a field to existing collection (use with caution - may corrupt GraphQL schema)
  • ↗List all collections
  • ↗Get collection schema
  • ↗Putting It All Together: Your First Vibe-Code Session
  • ↗1. Start your terminal (with Directus tokens in env)
  • ↗2. Launch Hermes agent
  • ↗3. Give the prompt (copy-paste from above)
  • ↗4. Agent thinks, generates script, executes
  • ↗5. You're done. Check the admin UI or query:
  • ↗What This Enables: Rapid Prototyping at Scale
  • ↗Limitations and Gotchas
  • ↗Not All Backend Patterns Fit Directus
  • ↗You Still Need to Think About Data Modeling
  • ↗Permissions Still Need Configuration
  • ↗Large Content Requires Patching
  • ↗Conclusion: The Backend Is Now a Conversation
  • ↗Next Steps
  • ↗Related Articles

Related Posts

8 Open-Source AI Tools You Missed This Week

From context compression to cross-platform AI agents — here are 8 powerful open-source projects reshaping the AI development landscape.

Necolas HamwiNecolas Hamwi
June 12, 2026 - 10 min read
OpenAI Dreaming V3 concept art - ChatGPT autonomous memory architecture

OpenAI's 'Dreaming V3' — ChatGPT Finally Has Persistent Memory

OpenAI's Dreaming V3 replaces saved memories with an autonomous background synthesis system. Factual recall jumps from 41.5% to 82.8%, compute drops 5x, and ChatGPT finally remembers like a thinking partner — not a notepad.

Necolas HamwiNecolas Hamwi
June 10, 2026 - 8 min read
Claude Fable 5: Anthropic Brings Mythos-Class Intelligence to the Public

Claude Fable 5: Anthropic Brings Mythos-Class Intelligence to the Public

Anthropic launched Claude Fable 5, the first publicly available Mythos-class model, delivering state-of-the-art intelligence with safety guardrails.

Necolas HamwiNecolas Hamwi
June 9, 2026 - 11 min read