# Why I Chose Directus as My Backend

When I started building the [Demo Store](https://demostore.northernrangedigital.com) — a full stack e-commerce app with TanStack Start — I needed a backend that could handle data, authentication, file storage, and an admin interface without stitching together multiple services.

Directus ticked every box. Here's why I chose it, how I set it up, and what it looks like in practice.

## How Directus Compares to Other CMS Options

There are plenty of headless CMS options out there. Here's how Directus stands out:

|  | Directus | Strapi | Contentful | Sanity |
| --- | --- | --- | --- | --- |
| Self-hostable | ✅ | ✅ | ❌ | ❌ |
| Free to use | ✅ under $5M revenue | ✅ open source | ❌ paid tiers | ❌ paid tiers |
| SQL DB support | ✅ any existing SQL DB | ✅ PostgreSQL, MySQL, MariaDB, SQLite | ❌ | ❌ |
| Built-in auth | ✅ | ✅ | ❌ | ❌ |
| REST + GraphQL | ✅ | ✅ | ✅ | ✅ |
| Admin dashboard | ✅ | ✅ | ✅ | ✅ |

Strapi is a strong alternative and supports multiple SQL databases too. The key difference for me was that Directus connects directly to **any existing database** — you point it at a database you already have and it works. There's no schema migration system to manage and no framework-specific setup. Contentful and Sanity are cloud-only, which means you don't own your data and you're subject to their pricing as you scale.

## Setting Up Directus with Docker Compose

I run Directus on a VPS managed by Coolify. The entire setup lives in a single Docker Compose file. Here's the full config I use for the demo store:

```yaml
services:
  directus:
    image: 'directus/directus:11.15.4'
    ports:
      - '8055:8055'
    volumes:
      - './uploads:/directus/uploads'
      - './extensions:/directus/extensions'
    environment:
      SECRET: '${SECRET}'
      MARKETPLACE_TRUST: all

      # Database
      DB_CLIENT: pg
      DB_HOST: '${DB_HOST}'
      DB_PORT: '5432'
      DB_DATABASE: '${DB_DATABASE}'
      DB_USER: '${DB_USER}'
      DB_PASSWORD: '${DB_PASSWORD}'

      # Redis Cache
      CACHE_ENABLED: 'false'
      CACHE_AUTO_PURGE: 'true'
      CACHE_STORE: redis
      REDIS: '${REDIS_URL}'

      # CORS
      CORS_ENABLED: 'true'
      CORS_ORIGIN: '${CORS_ORIGIN}'
      CORS_CREDENTIALS: 'true'

      # Email via Resend SMTP
      EMAIL_TRANSPORT: smtp
      EMAIL_SMTP_HOST: smtp.resend.com
      EMAIL_SMTP_PORT: 465
      EMAIL_SMTP_USER: resend
      EMAIL_SMTP_PASSWORD: '${EMAIL_SMTP_PASSWORD}'
      EMAIL_SMTP_SECURE: 'true'
      EMAIL_FROM: contact@yourdomain.com

      # Auth
      USER_REGISTER_REQUIRE_EMAIL_VERIFICATION: 'true'
      USER_REGISTER_URL_ALLOW_LIST: '${USER_REGISTER_URL_ALLOW_LIST}'
      PASSWORD_RESET_URL_ALLOW_LIST: '${PASSWORD_RESET_URL_ALLOW_LIST}'

      # Google OAuth
      AUTH_PROVIDERS: google
      AUTH_GOOGLE_DRIVER: openid
      AUTH_GOOGLE_ISSUER_URL: 'https://accounts.google.com'
      AUTH_GOOGLE_CLIENT_ID: '${AUTH_GOOGLE_CLIENT_ID}'
      AUTH_GOOGLE_CLIENT_SECRET: '${AUTH_GOOGLE_CLIENT_SECRET}'
      AUTH_GOOGLE_IDENTIFIER_KEY: email
      AUTH_GOOGLE_ALLOW_PUBLIC_REGISTRATION: 'true'
      AUTH_GOOGLE_DEFAULT_ROLE_ID: '${AUTH_GOOGLE_DEFAULT_ROLE_ID}'
      AUTH_GOOGLE_REDIRECT_ALLOW_LIST: '${AUTH_GOOGLE_REDIRECT_ALLOW_LIST}'
      AUTH_GOOGLE_MODE: session

      # Session & Cookies
      SESSION_COOKIE_SECURE: 'true'
      SESSION_COOKIE_SAME_SITE: lax
      REFRESH_TOKEN_COOKIE_SECURE: 'true'
      REFRESH_TOKEN_COOKIE_SAME_SITE: lax

      # Extensions
      EXTENSIONS_AUTO_RELOAD: 'true'
      EXTENSIONS_PATH: /directus/extensions
```

A few things worth noting:

*   **PostgreSQL** is the database — `DB_CLIENT: pg` tells Directus to use it
    
*   **Resend** handles all transactional email via SMTP — verification emails, password resets
    
*   **Google OAuth** is configured via OpenID Connect — users can register and log in with their Google account
    
*   **Session cookies** are secure and SameSite lax — works cleanly with TanStack Start on a separate domain
    
*   All sensitive values are environment variables — never hardcoded
    

> **Note:** In my setup PostgreSQL runs as a separate service managed independently by Coolify — that's why it's not included in this Compose file. If you prefer to keep everything together, you can add PostgreSQL directly to the same file:

```yaml
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: '${DB_DATABASE}'
      POSTGRES_USER: '${DB_USER}'
      POSTGRES_PASSWORD: '${DB_PASSWORD}'
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
```

Then set `DB_HOST` to `postgres` in your Directus environment variables to point it at the service name. Either approach works — separate or combined is a matter of preference.

## The Admin Dashboard and Collections

Once Directus is running, you access the admin dashboard at your Directus URL. This is where you build your data model visually.

For the demo store I created the following collections:

*   **products** — name, description (WYSIWYG), price, sale price, category, images, slug
    
*   **categories** — name, slug, description
    
*   **orders** — user, items, total, status
    
*   **order\_items** — order, product, quantity, price
    
*   **navigation** — links and structure for the site nav
    

Each collection maps directly to a PostgreSQL table. Directus generates the REST and GraphQL API automatically. No boilerplate, no migrations to write by hand.

The WYSIWYG editor on the `description` field outputs HTML — I cover how to render that safely in React in a later post in this series.

## Roles and Permissions

This is where Directus really shines for production apps. Every collection and field can have granular permissions set per role.

For the demo store I set up three roles:

**Public** — unauthenticated visitors

*   Can read `products`, `categories`, `navigation`
    
*   Cannot read `orders`, `order_items`, or any user data
    

**Authenticated User** — logged in customers

*   Can read all public collections
    
*   Can read and create their own `orders` and `order_items`
    
*   Can read and update their own user profile
    
*   Cannot read other users' data
    

**Admin** — full access

*   Full CRUD on all collections
    
*   Access to the Directus admin dashboard
    

The permissions are configured in the Directus admin panel under **Settings → Roles & Permissions**. You set them visually — no code required. Each role gets a matrix of read, create, update, delete permissions per collection, and you can even restrict access to specific fields within a collection.

## What Directus Handles For the Demo Store

*   ✅ All product and category data via REST API
    
*   ✅ User registration with email verification
    
*   ✅ Login with email/password and Google OAuth
    
*   ✅ Password reset via email
    
*   ✅ Order management
    
*   ✅ File and image storage and serving
    
*   ✅ Role-based access control
    
*   ✅ Admin dashboard for managing all content
    

All from a single self-hosted instance running on a $20/month VPS.

* * *

In the next post I'll cover how I connect TanStack Start to Directus using the SDK — setting up the client, typed data fetching, and filtering collections.

➡️ [Next: Connecting TanStack Start to Directus with the SDK](https://blog.northernrangedigital.com/connecting-tanstack-start-to-directus-with-the-sdk)
