<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged directus</title>
<link>https://coderlegion.com/tag/directus</link>
<description>Powered by Question2Answer</description>
<item>
<title>TanStack Start Basics Part 2 — Connecting to Your Directus Backend</title>
<link>https://coderlegion.com/26763/tanstack-start-basics-part-2-connecting-to-your-directus-backend</link>
<description>&lt;p&gt;Our frontend has been static so far — just hardcoded headings on a few pages. Today we connect it to the Directus backend built earlier in this series, using the official Directus SDK to pull real product data into our TanStack Start app.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=BUULSpRQiAY&quot;&gt;Youtube Video - Tanstack Start Basics Part 2&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;installing-the-directus-sdk&quot;&gt;Installing the Directus SDK&lt;/h3&gt;
&lt;p&gt;There are a few ways to talk to Directus from a frontend — the REST API directly, GraphQL — but the SDK is the cleanest option, and what we're using here.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @directus/sdk
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;creating-the-directus-helper&quot;&gt;Creating the Directus Helper&lt;/h3&gt;
&lt;p&gt;Inside &lt;code&gt;src/lib&lt;/code&gt;, create a new file: &lt;code&gt;directus.ts&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { createDirectus, rest } from '@directus/sdk'

export const directus = createDirectus('Your_Directus_Project_URL').with(rest())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;createDirectus&lt;/code&gt; initializes a connection client between your app and your Directus instance. On its own it starts empty — capabilities get added by chaining &lt;code&gt;.with()&lt;/code&gt;. Since we're fetching over standard HTTP REST requests, we pair it with the &lt;code&gt;rest()&lt;/code&gt; modifier.&lt;/p&gt;
&lt;h3 id=&quot;defining-our-first-type&quot;&gt;Defining Our First Type&lt;/h3&gt;
&lt;p&gt;Inside &lt;code&gt;src&lt;/code&gt;, create &lt;code&gt;types/index.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;export interface Products {
  id: string
  name: string
  brief: string
  description: string
  price: number
  sale: number
  quantity: number
  isOnSale: boolean
  category: string
  image: string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;writing-the-fetch-function&quot;&gt;Writing the Fetch Function&lt;/h3&gt;
&lt;p&gt;Back in &lt;code&gt;directus.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { createDirectus, rest, readItems } from '@directus/sdk'
import type { Products } from '@/types'

export const directus = createDirectus('Your_Directus_Project_URL').with(rest())

export async function getProducts(): Promise&amp;lt;Products[]&amp;gt; {
  return directus.request(
    readItems('products', {
      fields: [
        'id',
        'name',
        'brief',
        'description',
        'price',
        'sale',
        'quantity',
        'isOnSale',
        'category',
        'image',
      ],
    }),
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;readItems&lt;/code&gt; is a built-in SDK helper that generates a request for fetching multiple items from a given collection. At this point, TypeScript throws:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Type 'Record&amp;lt;string, any&amp;gt;[]' is not assignable to type 'Products[]'.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;fixing-the-schema-error&quot;&gt;Fixing the Schema Error&lt;/h3&gt;
&lt;p&gt;This happens because the SDK doesn't automatically know your database's actual shape — it needs a &lt;code&gt;Schema&lt;/code&gt; type passed into &lt;code&gt;createDirectus&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;type Schema = {
  products: Products[]
}

export const directus = createDirectus&amp;lt;Schema&amp;gt;('Your_Directus_Project_URL').with(rest())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This declares that a &lt;code&gt;products&lt;/code&gt; collection exists and maps to an array of &lt;code&gt;Products&lt;/code&gt;. With this in place, the error resolves — and as a bonus, TypeScript now validates and autocompletes the &lt;code&gt;fields&lt;/code&gt; array against this type too.&lt;/p&gt;
&lt;h3 id=&quot;fetching-the-data-method-1-tanstack-query&quot;&gt;Fetching the Data — Method 1: TanStack Query&lt;/h3&gt;
&lt;p&gt;Install it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm i @tanstack/react-query
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set it up in &lt;code&gt;__root.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

// Create a client
const queryClient = new QueryClient()

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    &amp;lt;html lang=&quot;en&quot;&amp;gt;
      &amp;lt;head&amp;gt;
        &amp;lt;HeadContent /&amp;gt;
      &amp;lt;/head&amp;gt;
      &amp;lt;body&amp;gt;
        &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
          {children}
        &amp;lt;/QueryClientProvider&amp;gt;
        &amp;lt;Scripts /&amp;gt;
      &amp;lt;/body&amp;gt;
    &amp;lt;/html&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Why does &lt;code&gt;RootDocument&lt;/code&gt; take a &lt;code&gt;children&lt;/code&gt; prop, and why wrap it?&lt;/strong&gt; &lt;code&gt;__root.tsx&lt;/code&gt; wraps your entire application — every route (About, Index, Products) gets passed in as &lt;code&gt;children&lt;/code&gt;, rendered inside this HTML shell. Wrapping &lt;code&gt;children&lt;/code&gt; with the query provider means every nested route and component now has access to TanStack Query.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now build the component, &lt;code&gt;src/components/Products.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { useQuery } from '@tanstack/react-query'

export default function Products() {
  const { data: products, isLoading: loading } = useQuery({
    queryKey: ['product'],
    queryFn: getProducts,
    staleTime: 5 * 60 * 1000,
    gcTime: 15 * 60 * 1000,
    refetchOnWindowFocus: false,
    retry: 2,
    refetchOnMount: true,
  })

  if (loading) {
    return &amp;lt;div className=&quot;container mx-auto p-8&quot;&amp;gt;Loading...&amp;lt;/div&amp;gt;
  }

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;div className=&quot;container mx-auto p-8&quot;&amp;gt;
        &amp;lt;h1 className=&quot;text-4xl font-medium mb-4&quot;&amp;gt;Products&amp;lt;/h1&amp;gt;
        &amp;lt;div className=&quot;grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6&quot;&amp;gt;
          {products?.map((product) =&amp;gt; (
            &amp;lt;div key={product.id} className=&quot;border rounded-lg p-4&quot;&amp;gt;
              &amp;lt;h2 className=&quot;text-xl font-bold mb-2&quot;&amp;gt;{product.name}&amp;lt;/h2&amp;gt;
            &amp;lt;/div&amp;gt;
          ))}
        &amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A quick rundown of the query options:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Option &lt;/th&gt;&lt;th&gt;What it does&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;staleTime&lt;/code&gt; &lt;/td&gt;&lt;td&gt;How long fetched data stays &quot;fresh&quot; (5 min) before a refetch is considered&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;gcTime&lt;/code&gt; &lt;/td&gt;&lt;td&gt;How long unused data stays cached in memory (15 min) before being cleared&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;refetchOnWindowFocus&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Whether switching tabs and back triggers a refetch (off here)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;retry&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Silent retry attempts on failure before giving up (2 here)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;refetchOnMount&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Whether the component checks for stale data and refetches on every mount (on here)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Wire it into the route:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import Products from '@/components/Products'
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/products')({
  component: RouteComponent,
})

function RouteComponent() {
  return (
    &amp;lt;div className=&quot;p-8&quot;&amp;gt;
      &amp;lt;h1 className=&quot;font-bold text-5xl text-slate-700&quot;&amp;gt;Products&amp;lt;/h1&amp;gt;
      &amp;lt;Products /&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Visit &lt;code&gt;localhost:3000/products&lt;/code&gt; — real product data, pulled live from Directus.&lt;/p&gt;
&lt;h3 id=&quot;fetching-the-data-method-2-route-loaders&quot;&gt;Fetching the Data — Method 2: Route Loaders&lt;/h3&gt;
&lt;p&gt;Loaders work a little differently, and only on routes directly — not on standalone components. Remove the &lt;code&gt;Products&lt;/code&gt; component from the route and build it in directly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import { createFileRoute } from '@tanstack/react-router'
import { getProducts } from '@/lib/directus'

export const Route = createFileRoute('/products')({
  loader: async () =&amp;gt; {
    const products = await getProducts()
    return products
  },
  component: RouteComponent,
})

function RouteComponent() {
  const products = Route.useLoaderData()

  return (
    &amp;lt;div className=&quot;p-8&quot;&amp;gt;
      &amp;lt;h1 className=&quot;font-bold text-5xl text-slate-700 mb-4&quot;&amp;gt;Products&amp;lt;/h1&amp;gt;
      &amp;lt;div className=&quot;grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6&quot;&amp;gt;
        {products?.map((product) =&amp;gt; (
          &amp;lt;div key={product.id} className=&quot;border rounded-lg p-4&quot;&amp;gt;
            &amp;lt;h2 className=&quot;text-xl font-bold mb-2&quot;&amp;gt;{product.name}&amp;lt;/h2&amp;gt;
          &amp;lt;/div&amp;gt;
        ))}
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The loader runs &lt;code&gt;getProducts&lt;/code&gt; before the route renders, and &lt;code&gt;Route.useLoaderData()&lt;/code&gt; pulls that resolved data straight into the component — no loading state needed, since the data's already there by the time the page shows up.&lt;/p&gt;
&lt;h3 id=&quot;which-one-should-you-use-&quot;&gt;Which One Should You Use?&lt;/h3&gt;
&lt;p&gt;Both approaches call the exact same &lt;code&gt;getProducts&lt;/code&gt; function underneath — the difference is &lt;em&gt;where&lt;/em&gt; the fetching happens and &lt;em&gt;how&lt;/em&gt; the result reaches your component.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TanStack Query&lt;/strong&gt; shines for data that changes often or benefits from caching, background refetching, and built-in loading states — great for a snappy feel on repeat visits.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Route loaders&lt;/strong&gt; fetch before the route even renders, so there's no loading spinner at all. That pairs particularly well with TanStack Start's server-side rendering model, since the data can be resolved as part of the initial render rather than after the fact.&lt;/p&gt;
&lt;h3 id=&quot;summary&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;In this part, we:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Installed and configured the Directus SDK&lt;/li&gt;
&lt;li&gt;Typed our data with a &lt;code&gt;Products&lt;/code&gt; interface and a &lt;code&gt;Schema&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Built our first real fetch function using &lt;code&gt;readItems&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Rendered a live products grid two different ways — TanStack Query and a route loader&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Right now we're only displaying each product's name, but this same pattern carries directly into fuller product cards next — pricing, images, sale badges, and eventually branding and tags once those relational fields come into play.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Found this useful? Follow for more Directus and TanStack Start tutorials, or check out the video version above.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26763/tanstack-start-basics-part-2-connecting-to-your-directus-backend</guid>
<pubDate>Thu, 10 Sep 2026 22:12:24 +0000</pubDate>
</item>
<item>
<title>Setting Up Our Frontend with TanStack Start</title>
<link>https://coderlegion.com/26506/setting-up-our-frontend-with-tanstack-start</link>
<description>&lt;p&gt;We've finished our backend series — a Directus instance running on a VPS managed by Coolify, backed by PostgreSQL and Redis, with a products collection already built and secured with roles and permissions.&lt;/p&gt;
&lt;p&gt;Now it's time to build a frontend to actually display that data. For this, we're using &lt;strong&gt;TanStack Start&lt;/strong&gt;, &lt;strong&gt;Tailwind CSS&lt;/strong&gt;, and &lt;strong&gt;shadcn/ui&lt;/strong&gt;. This post covers getting TanStack Start installed and running.&lt;/p&gt;
&lt;h3 id=&quot;what-is-tanstack-start-&quot;&gt;What Is TanStack Start?&lt;/h3&gt;
&lt;p&gt;Straight from the &lt;a rel=&quot;nofollow&quot; href=&quot;https://tanstack.com/start/latest/docs/framework/react/overview&quot;&gt;official docs&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;TanStack Start is a full-stack React framework powered by TanStack Router. It provides full-document SSR, streaming, server functions, client/server builds, and more. With Vite and Rsbuild support, it's ready to develop and deploy to the hosting provider or runtime you want.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In short: it offers the same kind of full-stack capability as frameworks like Next.js or Remix, built on top of TanStack Router.&lt;/p&gt;
&lt;h3 id=&quot;creating-the-project&quot;&gt;Creating the Project&lt;/h3&gt;
&lt;p&gt;First, create a folder for your app. I'm naming mine &lt;code&gt;app_fe&lt;/code&gt; — &quot;fe&quot; for front end.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Naming convention tip:&lt;/strong&gt; I use &lt;code&gt;&amp;lt;app-name&amp;gt;_fe&lt;/code&gt; for all my frontend project folders — so an e-commerce project would be &lt;code&gt;store_fe&lt;/code&gt;. Makes the folder's purpose obvious at a glance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;From inside that folder, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @tanstack/cli@latest create .
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note the trailing dot.&lt;/strong&gt; Since we created the folder ourselves beforehand, the dot tells the CLI to install directly into the current directory. Omit the dot if you'd rather the CLI create a new folder for you.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;walking-through-the-setup-prompts&quot;&gt;Walking Through the Setup Prompts&lt;/h3&gt;
&lt;p&gt;The CLI will walk you through a series of choices:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Prompt &lt;/th&gt;&lt;th&gt;Selection&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Select Framework &lt;/td&gt;&lt;td&gt;React&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Select Toolchain &lt;/td&gt;&lt;td&gt;ESLint&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Select Deployment Adapter &lt;/td&gt;&lt;td&gt;None (self-hosting on our own VPS)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Include demo/example pages? &lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;What add-ons would you like? &lt;/td&gt;&lt;td&gt;shadcn/ui&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Initialize a new git repository? &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Continue with these settings? &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Wait for the install to finish.&lt;/p&gt;
&lt;h3 id=&quot;running-the-app&quot;&gt;Running the App&lt;/h3&gt;
&lt;p&gt;Navigate into your app directory (if you aren't already there) and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open your browser to &lt;code&gt;http://localhost:3000&lt;/code&gt;. Congratulations — your TanStack Start app is up and running.&lt;/p&gt;
&lt;h3 id=&quot;editing-the-home-page&quot;&gt;Editing the Home Page&lt;/h3&gt;
&lt;p&gt;Open &lt;code&gt;src/routes/index.tsx&lt;/code&gt;. Delete everything inside the div, and save — your home page should now be completely blank. This file is your landing page, the first thing visitors see.&lt;/p&gt;
&lt;p&gt;Add an &lt;code&gt;h1&lt;/code&gt; tag inside the empty div:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;h1&amp;gt;Home&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save, and you'll see &quot;Home&quot; rendered on the page. Now let's add some Tailwind styling:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;h1 className=&quot;font-bold text-5xl text-slate-700&quot;&amp;gt;Home&amp;lt;/h1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save again — notice Tailwind works right out of the box, no extra configuration needed.&lt;/p&gt;
&lt;h3 id=&quot;file-based-routing&quot;&gt;File-Based Routing&lt;/h3&gt;
&lt;p&gt;TanStack Start runs on TanStack Router, which uses &lt;strong&gt;file-based routing&lt;/strong&gt;. Every file added to the &lt;code&gt;routes&lt;/code&gt; folder automatically becomes a navigable page.&lt;/p&gt;
&lt;h4 id=&quot;adding-an-about-page&quot;&gt;Adding an About Page&lt;/h4&gt;
&lt;p&gt;Create &lt;code&gt;src/routes/about.tsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div className=&quot;p-8&quot;&amp;gt;
  &amp;lt;h1 className=&quot;font-bold text-5xl text-slate-700&quot;&amp;gt;About&amp;lt;/h1&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save, then visit &lt;code&gt;http://localhost:3000/about&lt;/code&gt;. Notice the home page lives at &lt;code&gt;localhost:3000/&lt;/code&gt;, and the about page lives at &lt;code&gt;localhost:3000/about&lt;/code&gt; — TanStack Start handled that routing automatically, purely based on the filename.&lt;/p&gt;
&lt;h4 id=&quot;adding-a-products-page&quot;&gt;Adding a Products Page&lt;/h4&gt;
&lt;p&gt;Create &lt;code&gt;src/routes/products.tsx&lt;/code&gt; the same way:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;&amp;lt;div className=&quot;p-8&quot;&amp;gt;
  &amp;lt;h1 className=&quot;font-bold text-5xl text-slate-700&quot;&amp;gt;Products&amp;lt;/h1&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Visit &lt;code&gt;localhost:3000/products&lt;/code&gt;, and there's your new page. That's as simple as basic routing gets — there's plenty more you can do with nested and dynamic routes, which we'll cover as it becomes relevant.&lt;/p&gt;
&lt;h3 id=&quot;what-is-that-root-tsx-file-&quot;&gt;What Is That &lt;code&gt;__root.tsx&lt;/code&gt; File?&lt;/h3&gt;
&lt;p&gt;You've probably noticed a file called &lt;code&gt;__root.tsx&lt;/code&gt; sitting in your routes folder. Here's the simplest way to think about it:&lt;/p&gt;
&lt;p&gt;Picture any large website — Amazon, YouTube, anything. Every page has a header up top and often a footer at the bottom that never change, no matter what page you're on. But the content in the middle changes depending on where you navigated.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;__root.tsx&lt;/code&gt; is the file that builds that &quot;never changes&quot; part. It's the frame around every page in your app. Every route you build — home, about, products — gets rendered &lt;em&gt;inside&lt;/em&gt; this one file. Write it once, and every page automatically inherits it.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;A word of caution:&lt;/strong&gt; this file is central to how TanStack Router works under the hood. Avoid modifying it until you understand exactly what each piece does — we'll take a closer look at it in an upcoming post, exactly when it becomes relevant to what we're building.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;summary-1&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;In this part, we:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Installed and configured a new TanStack Start project&lt;/li&gt;
&lt;li&gt;Ran it locally and confirmed Tailwind works out of the box&lt;/li&gt;
&lt;li&gt;Learned file-based routing by building About and Products pages&lt;/li&gt;
&lt;li&gt;Got introduced to &lt;code&gt;__root.tsx&lt;/code&gt;, the file behind every page in the app&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That wraps up our frontend setup. Next up, we'll start pulling real data from our Directus backend into this frontend — connecting everything we've built across this series.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Found this useful? Follow for more Directus and TanStack Start tutorials, or check out the video version above.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26506/setting-up-our-frontend-with-tanstack-start</guid>
<pubDate>Tue, 08 Sep 2026 15:45:44 +0000</pubDate>
</item>
<item>
<title>From Retrenchment to Full-Stack Developer: Wade Thomas on Learning, Simplicity and Building Software</title>
<link>https://coderlegion.com/25749/from-retrenchment-full-stack-developer-wade-thomas-learning-simplicity-building-software</link>
<description>&lt;h2 id=&quot;from-retrenchment-to-full-stack-developer-wade-thomas-on-learning-simplicity-and-building-software-that-matters&quot;&gt;From Retrenchment to Full-Stack Developer: Wade Thomas on Learning, Simplicity and Building Software That Matters&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Developer Stories | CoderLegion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;What happens when a career change forces you to rethink your future?&lt;/p&gt;
&lt;p&gt;For Wade Alister Thomas, the answer was to turn a difficult setback into the beginning of a completely different career.&lt;/p&gt;
&lt;p&gt;After spending 18 years working in the telecommunications industry, Wade was among approximately 500 employees who were retrenched in November 2018. He had recently started a family and finished building his home, so the timing could hardly have been more challenging.&lt;/p&gt;
&lt;p&gt;At that point, Wade had a decision to make: look for another traditional job or take the small coding projects he had been doing on the side and turn them into a full-time career.&lt;/p&gt;
&lt;p&gt;He chose the second path.&lt;/p&gt;
&lt;p&gt;Today, Wade is a self-taught full-stack developer and consultant based in Trinidad &amp;amp; Tobago. His preferred stack includes TanStack Start, Tailwind CSS and shadcn/ui on the frontend, with Directus, PostgreSQL and Redis running on a VPS through Coolify.&lt;/p&gt;
&lt;p&gt;He also works with businesses on their digital presence, builds applications, creates logos and Git templates, and regularly shares what he learns through technical articles.&lt;/p&gt;
&lt;p&gt;In this Developer Stories interview, Wade talks about his journey from telecommunications to software development, the challenges of learning without a traditional curriculum, why he prefers simple and sustainable architectures, what attracted him to self-hosting, and why developers should focus on solving business problems rather than simply using the newest technology.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;from-2014-to-2018-when-a-career-changed-direction&quot;&gt;From 2014 to 2018: When a Career Changed Direction&lt;/h3&gt;
&lt;p&gt;Wade started coding in 2014, initially taking on small projects for friends and family.&lt;/p&gt;
&lt;p&gt;At the time, however, software development was still something he was doing alongside his full-time career in telecommunications.&lt;/p&gt;
&lt;p&gt;That changed in 2018.&lt;/p&gt;
&lt;p&gt;About 500 employees at the telecommunications company were retrenched on November 15, 2018, and Wade was among them.&lt;/p&gt;
&lt;p&gt;It was a particularly difficult period because he had recently started a family and had just finished building his home.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;It was a really difficult time, as I had started a family and just finished building our home.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After 18 years in telecommunications, Wade had to decide whether to look for another traditional job or pursue the coding work he had already started doing on the side.&lt;/p&gt;
&lt;p&gt;With the support of his wife and after much prayer, he decided to pursue software development full-time.&lt;/p&gt;
&lt;p&gt;That decision marked the beginning of a completely different professional journey.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-challenge-of-being-a-self-taught-developer&quot;&gt;The Challenge of Being a Self-Taught Developer&lt;/h3&gt;
&lt;p&gt;Being self-taught gave Wade freedom, but it also created one of the biggest challenges many independent developers face: deciding what to learn.&lt;/p&gt;
&lt;p&gt;There was no university curriculum, mentor or structured program telling him which technology to choose next.&lt;/p&gt;
&lt;p&gt;He initially enrolled with Treehouse but eventually had to stop because he could not afford the monthly subscription.&lt;/p&gt;
&lt;p&gt;He began learning JavaScript, HTML and CSS, purchasing discounted courses on Udemy and experimenting with different technologies.&lt;/p&gt;
&lt;p&gt;Eventually, he realized that knowing frontend technologies alone wasn't enough.&lt;/p&gt;
&lt;p&gt;He needed to understand backend development as well.&lt;/p&gt;
&lt;p&gt;The problem was that there was nobody to point him toward a specific path.&lt;/p&gt;
&lt;p&gt;Before AI made technical research much easier, Wade had to search through the internet himself, experiment, make mistakes and figure things out through trial and error.&lt;/p&gt;
&lt;p&gt;He eventually settled on React for the frontend and Node/Express for the backend.&lt;/p&gt;
&lt;p&gt;But the experimentation didn't stop there.&lt;/p&gt;
&lt;p&gt;He explored Vue.js, returned to React, and tried several frameworks including Next.js, Remix, Redwood and TanStack.&lt;/p&gt;
&lt;p&gt;Then came another area of exploration: headless CMS platforms.&lt;/p&gt;
&lt;p&gt;He experimented with Contentful, Strapi, DatoCMS, Sanity, Storyblok, Prismic, Payload and eventually Directus.&lt;/p&gt;
&lt;p&gt;Looking back, Wade says deciding what to learn and sticking with it was a major struggle.&lt;/p&gt;
&lt;p&gt;What eventually helped was finding a technology stack with a manageable learning curve and, more importantly, enjoying the process of learning.&lt;/p&gt;
&lt;p&gt;The motivation shifted from simply trying to reach an outcome to genuinely enjoying the journey.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;finding-a-stack-that-works&quot;&gt;Finding a Stack That Works&lt;/h3&gt;
&lt;p&gt;Wade's current technology stack didn't appear overnight.&lt;/p&gt;
&lt;p&gt;It evolved through real client work and the practical problems that came with building software as a solo developer.&lt;/p&gt;
&lt;p&gt;His first paying client was a travel agency.&lt;/p&gt;
&lt;p&gt;At the time, Wade was using React on the frontend and Node/Express on the backend.&lt;/p&gt;
&lt;p&gt;That meant manually building many of the pieces required by the application, including forms, APIs and database functionality.&lt;/p&gt;
&lt;p&gt;The workload was significant.&lt;/p&gt;
&lt;p&gt;He didn't meet the original delivery date, although he ultimately delivered a working product.&lt;/p&gt;
&lt;p&gt;That experience taught him something important about working as a solo developer: the technology stack itself can have a major impact on how quickly and sustainably you can deliver.&lt;/p&gt;
&lt;p&gt;Instead of simply accepting the complexity, Wade began researching alternatives.&lt;/p&gt;
&lt;p&gt;He wanted technologies that worked well together and allowed him to spend more time building the actual application instead of repeatedly wiring infrastructure together.&lt;/p&gt;
&lt;p&gt;His current approach focuses on technologies including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TanStack Start&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;shadcn/ui&lt;/li&gt;
&lt;li&gt;Directus&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;VPS infrastructure&lt;/li&gt;
&lt;li&gt;Coolify&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal isn't to use the most impressive stack possible.&lt;/p&gt;
&lt;p&gt;It is to use a stack that works well for the types of clients he serves.&lt;/p&gt;
&lt;p&gt;Because Wade primarily works with startups and small-to-medium businesses, he doesn't believe every project needs an enterprise-level architecture.&lt;/p&gt;
&lt;p&gt;Instead, he looks for solutions that are affordable, sustainable and fast to deliver.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-wade-chose-self-hosting&quot;&gt;Why Wade Chose Self-Hosting&lt;/h3&gt;
&lt;p&gt;Many developers today immediately reach for managed platforms such as Vercel or Netlify.&lt;/p&gt;
&lt;p&gt;Wade took a different route.&lt;/p&gt;
&lt;p&gt;He initially used Netlify for some of his projects, but eventually became frustrated with aspects of the platform, including pricing, DNS management and deployment errors that weren't always intuitive.&lt;/p&gt;
&lt;p&gt;The possibility of unpredictable hosting costs also made him cautious about some managed platforms.&lt;/p&gt;
&lt;p&gt;Self-hosting offered a different model.&lt;/p&gt;
&lt;p&gt;With a VPS and Coolify, Wade could control the hosting environment while still keeping much of the convenience developers expect from modern deployment platforms.&lt;/p&gt;
&lt;p&gt;He could create subdomains, automatically manage SSL certificates, run frontend and backend applications on the same server, and deploy directly from GitHub.&lt;/p&gt;
&lt;p&gt;For the types of businesses he works with, this provided a practical balance between flexibility, cost and simplicity.&lt;/p&gt;
&lt;p&gt;But Wade sees another important benefit to self-hosting.&lt;/p&gt;
&lt;p&gt;It forces developers to understand what actually happens when an application runs in production.&lt;/p&gt;
&lt;p&gt;You have to understand the server, deployment process, database and infrastructure.&lt;/p&gt;
&lt;p&gt;That knowledge can be valuable even when working with managed platforms.&lt;/p&gt;
&lt;p&gt;For Wade, predictable costs are another major advantage.&lt;/p&gt;
&lt;p&gt;Instead of worrying that an unexpected traffic spike or usage metric could suddenly increase a hosting bill, he knows what infrastructure he is paying for.&lt;/p&gt;
&lt;p&gt;Self-hosting also reduces dependence on a particular provider's pricing and platform rules.&lt;/p&gt;
&lt;p&gt;At the same time, Coolify provides many of the conveniences developers appreciate from managed platforms while allowing Wade to retain control over his infrastructure.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-directus-became-an-important-part-of-his-stack&quot;&gt;Why Directus Became an Important Part of His Stack&lt;/h3&gt;
&lt;p&gt;Before using Directus, Wade built backend systems using Node/Express.&lt;/p&gt;
&lt;p&gt;That meant creating APIs, middleware, authentication and authorization himself.&lt;/p&gt;
&lt;p&gt;For a solo developer, that can add a considerable amount of work to every project.&lt;/p&gt;
&lt;p&gt;Directus changed that equation.&lt;/p&gt;
&lt;p&gt;With Directus, Wade could quickly create a backend and automatically expose APIs through REST, GraphQL or the SDK.&lt;/p&gt;
&lt;p&gt;The platform also provides a data studio that allows clients and non-developers to manage their own data.&lt;/p&gt;
&lt;p&gt;Authentication is available out of the box, and Directus can connect to existing SQL databases.&lt;/p&gt;
&lt;p&gt;For Wade, this makes Directus particularly useful for content-driven websites and fast-turnaround MVPs where speed and a client-friendly data management experience are important.&lt;/p&gt;
&lt;p&gt;It also fits naturally into his self-hosting approach.&lt;/p&gt;
&lt;p&gt;However, Wade doesn't see Directus as the answer to every backend problem.&lt;/p&gt;
&lt;p&gt;For applications with highly complex business logic, he believes a custom backend can still be the better choice.&lt;/p&gt;
&lt;p&gt;That distinction is important.&lt;/p&gt;
&lt;p&gt;The goal isn't to find one technology that solves every problem.&lt;/p&gt;
&lt;p&gt;The goal is to choose the right level of complexity for the problem being solved.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-simplicity-becomes-an-architecture-principle&quot;&gt;When Simplicity Becomes an Architecture Principle&lt;/h3&gt;
&lt;p&gt;One of the strongest themes in Wade's approach to development is simplicity.&lt;/p&gt;
&lt;p&gt;Developers can easily become distracted by the desire to build something technically impressive.&lt;/p&gt;
&lt;p&gt;Modern frameworks, animations, advanced architectures and new technologies can all be exciting.&lt;/p&gt;
&lt;p&gt;But they can also distract from the actual purpose of a product.&lt;/p&gt;
&lt;p&gt;Wade believes in launching a minimum viable product, keeping the initial implementation focused and adding complexity only when it becomes necessary.&lt;/p&gt;
&lt;p&gt;Complexity has a cost.&lt;/p&gt;
&lt;p&gt;It can increase:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bugs&lt;/li&gt;
&lt;li&gt;Development time&lt;/li&gt;
&lt;li&gt;Testing requirements&lt;/li&gt;
&lt;li&gt;Maintenance work&lt;/li&gt;
&lt;li&gt;Project costs&lt;/li&gt;
&lt;li&gt;Time required to get customer feedback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;He experienced this mindset firsthand while building his own portfolio.&lt;/p&gt;
&lt;p&gt;His initial idea was to create an impressive interactive website with transitions, parallax scrolling, dark mode, 3D images and animations.&lt;/p&gt;
&lt;p&gt;Instead, he stepped back and asked what the portfolio actually needed to accomplish.&lt;/p&gt;
&lt;p&gt;The purpose was simple: convince a potential client that he could build software that solved real-world problems.&lt;/p&gt;
&lt;p&gt;So he created a much simpler interface and focused on examples of applications he had built.&lt;/p&gt;
&lt;p&gt;The result was a faster launch and a clearer demonstration of his actual capabilities.&lt;/p&gt;
&lt;p&gt;The lesson was simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A product doesn't need to demonstrate everything a developer knows. It needs to solve the problem it was created to solve.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;from-developer-to-consultant&quot;&gt;From Developer to Consultant&lt;/h3&gt;
&lt;p&gt;Working directly with clients changed Wade's understanding of software development.&lt;/p&gt;
&lt;p&gt;As a developer, it's easy to focus primarily on code.&lt;/p&gt;
&lt;p&gt;But clients aren't usually buying code.&lt;/p&gt;
&lt;p&gt;They're buying a solution to a business problem.&lt;/p&gt;
&lt;p&gt;Wade learned that understanding the client's industry, customers and business goals is just as important as understanding the technology.&lt;/p&gt;
&lt;p&gt;A web application is often not the business itself.&lt;/p&gt;
&lt;p&gt;Instead, it becomes a tool that helps the business compete, communicate with customers or operate more effectively.&lt;/p&gt;
&lt;p&gt;That means technology decisions should follow business requirements rather than dictate them.&lt;/p&gt;
&lt;p&gt;Wade puts it simply:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;The technology must serve the solution, not the other way around.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This also means that the technically superior solution isn't always the right solution.&lt;/p&gt;
&lt;p&gt;Sometimes the newest or most advanced technology is also the most expensive.&lt;/p&gt;
&lt;p&gt;If that cost doesn't make sense for the business, Wade would rather recommend a proven and affordable solution.&lt;/p&gt;
&lt;p&gt;The best architecture isn't necessarily the one with the most impressive technology.&lt;/p&gt;
&lt;p&gt;Sometimes it is the one that makes the most sense for the business.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;building-more-than-software&quot;&gt;Building More Than Software&lt;/h3&gt;
&lt;p&gt;Wade's work extends beyond writing code.&lt;/p&gt;
&lt;p&gt;He also works with UI/UX, ecommerce, performance, authentication, security, branding and logo design.&lt;/p&gt;
&lt;p&gt;For him, understanding the user is a critical part of building software.&lt;/p&gt;
&lt;p&gt;He gave the example of working with an ecommerce client selling natural self-care products.&lt;/p&gt;
&lt;p&gt;Rather than immediately starting with the interface, Wade became a customer himself.&lt;/p&gt;
&lt;p&gt;He bought and experienced the products, visited the client's pop-up events and observed the customers.&lt;/p&gt;
&lt;p&gt;That helped him understand the product, the customers and the image the business wanted to communicate.&lt;/p&gt;
&lt;p&gt;He could then use that understanding to shape the user interface and create a persona for the ideal customer.&lt;/p&gt;
&lt;p&gt;This reinforced an important lesson:&lt;/p&gt;
&lt;p&gt;A technically flawless application is not necessarily a successful product.&lt;/p&gt;
&lt;p&gt;If nobody wants to use it, the quality of the code doesn't matter nearly as much.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-wade-shares-what-he-learns&quot;&gt;Why Wade Shares What He Learns&lt;/h3&gt;
&lt;p&gt;Wade's development journey wasn't always easy.&lt;/p&gt;
&lt;p&gt;There were periods when he almost gave up.&lt;/p&gt;
&lt;p&gt;One of the challenges he faced was the cost of education and development resources, particularly because of currency conversion. At one point, the exchange rate made many courses difficult to afford.&lt;/p&gt;
&lt;p&gt;He also remembers a time before AI made technical research much easier.&lt;/p&gt;
&lt;p&gt;Finding reliable information required significant searching and experimentation.&lt;/p&gt;
&lt;p&gt;That experience influenced the way Wade approaches sharing knowledge today.&lt;/p&gt;
&lt;p&gt;His goal is straightforward:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Share the tools he uses, explain why he uses them, and show others how to use them.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;He wants to make the learning curve easier for someone who might be considering web development as a career path.&lt;/p&gt;
&lt;p&gt;There are now countless frameworks, platforms and tools available, and that abundance can itself become overwhelming.&lt;/p&gt;
&lt;p&gt;Wade wants his content to help developers make sense of those choices rather than simply telling them which technology is &quot;the best.&quot;&lt;/p&gt;
&lt;p&gt;Teaching has also improved his own understanding.&lt;/p&gt;
&lt;p&gt;Explaining a concept forces you to understand it more deeply.&lt;/p&gt;
&lt;p&gt;For Wade, documenting his learning has therefore become part of his own development process.&lt;/p&gt;
&lt;p&gt;The more he teaches, the better he understands the technologies he works with.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;advice-for-self-taught-developers&quot;&gt;Advice for Self-Taught Developers&lt;/h3&gt;
&lt;p&gt;For developers starting their journey today, Wade has a few practical recommendations.&lt;/p&gt;
&lt;p&gt;First, choose a programming language with a strong community and learn the fundamentals well.&lt;/p&gt;
&lt;p&gt;Don't make learning feel like a punishment.&lt;/p&gt;
&lt;p&gt;There will always be difficult periods, but there should also be some enjoyment in the process.&lt;/p&gt;
&lt;p&gt;Second, take advantage of the huge amount of free educational material available today.&lt;/p&gt;
&lt;p&gt;YouTube has become a valuable learning resource, and AI can also be used as a learning assistant.&lt;/p&gt;
&lt;p&gt;Wade recommends exhausting free resources before spending money on courses.&lt;/p&gt;
&lt;p&gt;Third, use development roadmaps.&lt;/p&gt;
&lt;p&gt;One of the biggest challenges he faced was not knowing what to learn next.&lt;/p&gt;
&lt;p&gt;Today, developers have access to many structured roadmaps that can provide direction.&lt;/p&gt;
&lt;p&gt;Perhaps most importantly, Wade recommends avoiding the temptation to become a jack of all trades.&lt;/p&gt;
&lt;p&gt;Find a technology stack you enjoy and become highly capable with it before constantly adding new technologies.&lt;/p&gt;
&lt;p&gt;Don't chase every new framework simply because it is popular.&lt;/p&gt;
&lt;p&gt;Change technologies when there is a real reason to do so and when the change genuinely improves your development process.&lt;/p&gt;
&lt;p&gt;And when building applications, focus on structure rather than obsessing over syntax.&lt;/p&gt;
&lt;p&gt;Before writing code, understand what you want to achieve.&lt;/p&gt;
&lt;p&gt;Break the application into components and understand how those components need to interact.&lt;/p&gt;
&lt;p&gt;Then start writing the code.&lt;/p&gt;
&lt;p&gt;As Wade puts it, understanding structure can eliminate many of the problems that inexperienced developers encounter.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-journey-built-on-learning-and-purpose&quot;&gt;A Journey Built on Learning and Purpose&lt;/h3&gt;
&lt;p&gt;Wade's journey from telecommunications to full-stack development wasn't planned in a traditional way.&lt;/p&gt;
&lt;p&gt;It began with small coding projects, continued through a difficult career transition, and developed through years of self-directed experimentation.&lt;/p&gt;
&lt;p&gt;Along the way, he learned that technology is only one part of software development.&lt;/p&gt;
&lt;p&gt;Choosing the right tools matters.&lt;/p&gt;
&lt;p&gt;Understanding infrastructure matters.&lt;/p&gt;
&lt;p&gt;Learning continuously matters.&lt;/p&gt;
&lt;p&gt;But understanding the business, the customer and the problem you're trying to solve may matter even more.&lt;/p&gt;
&lt;p&gt;His approach today reflects that experience: choose practical technologies, avoid unnecessary complexity, control infrastructure where it makes sense, understand the user and keep learning.&lt;/p&gt;
&lt;p&gt;For developers who feel overwhelmed by the constant stream of new frameworks and technologies, Wade's story offers a useful reminder:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You don't need to learn everything. You need to learn enough to solve meaningful problems well.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;And perhaps his most personal piece of advice comes from the principle that has helped him throughout the journey:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;As for my secret weapon — whenever I'm stuck looking for a solution, I pray.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;about-wade-alister-thomas&quot;&gt;About Wade Alister Thomas&lt;/h3&gt;
&lt;p&gt;His coderlegion &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/user/wadethomastt&quot;&gt;profile&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Wade Alister Thomas is a self-taught full-stack developer and consultant from Trinidad &amp;amp; Tobago.&lt;/p&gt;
&lt;p&gt;His preferred technology stack includes TanStack Start, Tailwind CSS, shadcn/ui, Directus, PostgreSQL, Redis and Coolify-managed VPS infrastructure.&lt;/p&gt;
&lt;p&gt;Through Northern Range Digital, he helps businesses establish their online presence and build digital products. He also creates logos and Git templates and regularly shares what he learns through technical articles and guides.&lt;/p&gt;
&lt;p&gt;You can learn more about Wade and his work through his CoderLegion profile and website.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25749/from-retrenchment-full-stack-developer-wade-thomas-learning-simplicity-building-software</guid>
<pubDate>Sat, 29 Aug 2026 09:12:49 +0000</pubDate>
</item>
<item>
<title>Directus Basics Part 3 — User Roles &amp; Permissions</title>
<link>https://coderlegion.com/25060/directus-basics-part-3-user-roles-permissions</link>
<description>&lt;p&gt;This is part three of our Directus Basics series. In &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=tJGuxqhv2SY&amp;amp;t=12s&quot;&gt;part one&lt;/a&gt; we set up our instance, and in &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=7SNrBzaxreg&amp;amp;t=108s&quot;&gt;part two&lt;/a&gt; we covered relationships between collections. Today we're covering access control — what determines who can see and touch your data.&lt;/p&gt;
&lt;p&gt;Part III - &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=qAk3X4y8v1U&quot;&gt;Youtube&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-core-concepts&quot;&gt;The Core Concepts&lt;/h3&gt;
&lt;p&gt;Access control in Directus comes down to three terms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Permission&lt;/strong&gt; — applies to one collection and one action (create, read, update, delete, or share). Can be full access, no access, or custom rules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy&lt;/strong&gt; — a group of permissions bundled together, applied to users or roles.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Role&lt;/strong&gt; — defines a user's position within a project. A role can hold any number of policies, apply to any number of users, and have child roles of its own.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-happens-without-a-role-&quot;&gt;What Happens Without a Role?&lt;/h3&gt;
&lt;p&gt;If an administrator creates a user but doesn't assign a role, that user has valid login credentials but still can't access the Data Studio.&lt;/p&gt;
&lt;p&gt;Creating a role and assigning the user to it isn't enough either — logging in at that point returns a &lt;strong&gt;&quot;No App Access&quot;&lt;/strong&gt; error. The missing piece is that the role has no &lt;strong&gt;access policy&lt;/strong&gt; attached. The policy is what actually tells the role what its users can and can't do.&lt;/p&gt;
&lt;h3 id=&quot;the-access-order&quot;&gt;The Access Order&lt;/h3&gt;
&lt;p&gt;Access in Directus flows in a specific direction:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Access Policy → Role → User&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In practice: define what level of access a user needs, build an access policy to match, create a role and attach that policy to it, then register the user and assign them to the role.&lt;/p&gt;
&lt;h3 id=&quot;default-policies&quot;&gt;Default Policies&lt;/h3&gt;
&lt;p&gt;Every fresh Directus instance ships with two default policies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Administrator&lt;/strong&gt; — the role you're signed in as by default, with unrestricted access to everything.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Public&lt;/strong&gt; — for data that should be visible without logging in. Think of a product catalog on an e-commerce site — forcing a login just to browse products is a poor experience.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; give the Public policy Read access only. Never Create, Update, or Delete. The public should be able to view data, never manipulate it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;giving-a-collection-public-access&quot;&gt;Giving a Collection Public Access&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Access Policies&lt;/strong&gt; → &lt;strong&gt;Public&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Under Permissions, click &lt;strong&gt;Add Collection&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;products&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Read&lt;/strong&gt;, choose &lt;strong&gt;All Access&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;creating-a-custom-access-policy&quot;&gt;Creating a Custom Access Policy&lt;/h3&gt;
&lt;p&gt;Let's build a policy for a data-entry team member with limited access.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Access Policies&lt;/strong&gt;, click &lt;strong&gt;Create&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Name it &lt;code&gt;DataEntry&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Check &lt;strong&gt;App Access&lt;/strong&gt; — leave &lt;strong&gt;Admin Access&lt;/strong&gt; unchecked, since that grants unrestricted control&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Directus populates the policy with minimum defaults for its own system collections, but nothing for your custom collections yet.&lt;/p&gt;
&lt;p&gt;Add products to it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Add Collection&lt;/strong&gt;, select &lt;code&gt;products&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Read&lt;/strong&gt;, choose &lt;strong&gt;All Access&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;creating-the-user-role&quot;&gt;Creating the User Role&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;User Roles&lt;/strong&gt;, click &lt;strong&gt;Create&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Name it &lt;code&gt;DataEntry&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Under Policies, click &lt;strong&gt;Add Existing&lt;/strong&gt;, select the &lt;code&gt;DataEntry&lt;/code&gt; policy&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;creating-the-user&quot;&gt;Creating the User&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;User Directory&lt;/strong&gt;, click &lt;strong&gt;Create&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Fill in first name, last name, email, and password&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Role&lt;/strong&gt;, select &lt;code&gt;DataEntry&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you check back on &lt;strong&gt;User Roles&lt;/strong&gt;, you'll see a &lt;code&gt;1&lt;/code&gt; next to DataEntry under Users, and a matching &lt;code&gt;1&lt;/code&gt; under Access Policies. That confirms the role, policy, and user are all wired together correctly.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; giving your policy and role the same name (as done here with &quot;DataEntry&quot;) makes them much easier to track as your project grows.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;testing-read-only-access&quot;&gt;Testing Read-Only Access&lt;/h3&gt;
&lt;p&gt;Log in as the new user. You'll see the products collection, but attempting to edit any field does nothing — expected, since only Read access was granted.&lt;/p&gt;
&lt;p&gt;Scrolling to the bottom of a product's detail page, some data is simply missing — those fields belong to &lt;em&gt;other&lt;/em&gt; collections the user doesn't yet have permission for. The product image is missing too.&lt;/p&gt;
&lt;h3 id=&quot;fixing-the-missing-file-permission&quot;&gt;Fixing the Missing File Permission&lt;/h3&gt;
&lt;p&gt;Back in the &lt;code&gt;DataEntry&lt;/code&gt; policy, notice &lt;strong&gt;&lt;code&gt;directus_files&lt;/code&gt;&lt;/strong&gt; isn't included among the accessible system collections. Without it, uploaded files — including product images — stay invisible.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Add Collection&lt;/strong&gt;, select &lt;code&gt;directus_files&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Read&lt;/strong&gt;, choose &lt;strong&gt;All Access&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Log back in as the data-entry user, and the product image now appears.&lt;/p&gt;
&lt;h4 id=&quot;adding-the-thumbnail-to-list-view&quot;&gt;Adding the Thumbnail to List View&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;In list view, click the &lt;strong&gt;+&lt;/strong&gt; icon on the right&lt;/li&gt;
&lt;li&gt;Scroll to &lt;strong&gt;image&lt;/strong&gt;, click the arrow next to it — not the field name itself, or you'll add the raw file ID instead of the thumbnail&lt;/li&gt;
&lt;li&gt;Scroll to &lt;strong&gt;thumbnail&lt;/strong&gt;, click outside the dropdown to confirm&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Drag column headers to reorder them as needed.&lt;/p&gt;
&lt;h3 id=&quot;extending-access-to-related-collections&quot;&gt;Extending Access to Related Collections&lt;/h3&gt;
&lt;p&gt;The same missing-permission pattern applies to &lt;strong&gt;branding&lt;/strong&gt;, &lt;strong&gt;products_tags&lt;/strong&gt; and &lt;strong&gt;tags&lt;/strong&gt; — add all to the &lt;code&gt;DataEntry&lt;/code&gt; policy with Read access, and the relational fields on products become visible.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt; you can't access a relational field in one collection unless you also have permission on the &lt;em&gt;related&lt;/em&gt; collection. Both sides of the relationship need coverage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;custom-field-level-permissions&quot;&gt;Custom Field-Level Permissions&lt;/h3&gt;
&lt;p&gt;Sometimes all-or-nothing collection access isn't granular enough. Directus lets you restrict access down to individual fields.&lt;/p&gt;
&lt;p&gt;Let's allow the data-entry user to update only the &lt;code&gt;name&lt;/code&gt; field on products:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;On the products collection, click the &lt;strong&gt;Update&lt;/strong&gt; action&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Use Custom&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Field Permissions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Check &lt;strong&gt;Name&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Save, then save again&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Logging back in, the data-entry user can now edit the Name field — and nothing else on that collection.&lt;/p&gt;
&lt;h3 id=&quot;summary-2&quot;&gt;Summary&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Concept    &lt;/th&gt;&lt;th&gt;Definition&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Permission &lt;/td&gt;&lt;td&gt;One collection + one action, set to full/none/custom&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Policy     &lt;/td&gt;&lt;td&gt;A bundle of permissions, applied to users or roles&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Role       &lt;/td&gt;&lt;td&gt;A user's position in the project; holds policies, applies to users&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Access control in Directus is genuinely one of its most powerful features — this covers the fundamentals, but there's plenty more granularity available as your project grows.&lt;/p&gt;
&lt;p&gt;That wraps part three. Next up, we'll look at how everything from this series comes together once we start pulling this data into a real front end.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Found this useful? Follow for more Directus and TanStack Start tutorials, or check out the video version above.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25060/directus-basics-part-3-user-roles-permissions</guid>
<pubDate>Fri, 21 Aug 2026 06:23:23 +0000</pubDate>
</item>
<item>
<title>Directus Basics Part 2 — Understanding Relationships (M2O, O2M, M2M)</title>
<link>https://coderlegion.com/24803/directus-basics-part-2-understanding-relationships-m2o-o2m-m2m</link>
<description>&lt;p&gt;This is part two of our Directus Basics series. In &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=tJGuxqhv2SY&amp;amp;t=6s&quot;&gt;part one&lt;/a&gt;, we set up a Directus instance and created our first collection. Today we're covering one of the most powerful features Directus offers: &lt;strong&gt;relationships&lt;/strong&gt; between collections.&lt;/p&gt;
&lt;p&gt;By the end of this article, you'll understand three core relationship types — Many to One (M2O), One to Many (O2M), and Many to Many (M2M). There's a fourth type, Many to Any (M2A), which we'll cover in a future post.&lt;/p&gt;
&lt;h3 id=&quot;setting-up-the-branding-collection&quot;&gt;Setting Up the Branding Collection&lt;/h3&gt;
&lt;p&gt;Continuing from our existing Directus instance, let's create a new collection called &lt;strong&gt;branding&lt;/strong&gt;. Select all the additional default fields Directus offers.&lt;/p&gt;
&lt;p&gt;Add two fields:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Field &lt;/th&gt;&lt;th&gt;Type&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;name&lt;/code&gt; &lt;/td&gt;&lt;td&gt;String (basic input)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;description&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Text (textarea)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Once the collection is set up, add a few entries — I'd recommend at least two different brands so you can see the relationship in action once it's wired up.&lt;/p&gt;
&lt;h3 id=&quot;many-to-one-m2o-products-branding&quot;&gt;Many to One (M2O): Products → Branding&lt;/h3&gt;
&lt;p&gt;Head over to your &lt;strong&gt;products&lt;/strong&gt; collection. We want to connect a product to a brand.&lt;/p&gt;
&lt;p&gt;Logically: many products can belong to &lt;em&gt;one&lt;/em&gt; brand. That's a Many to One relationship — many products, to one brand.&lt;/p&gt;
&lt;p&gt;Steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create Field → Relational category → &lt;strong&gt;Many to One&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Key: &lt;code&gt;brand_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Related Collection: &lt;code&gt;branding&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;De-select &lt;strong&gt;Enable Create Button&lt;/strong&gt; — you always want to choose a brand from an existing list, not create one on the fly&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;one-to-many-o2m-branding-products&quot;&gt;One to Many (O2M): Branding → Products&lt;/h3&gt;
&lt;p&gt;Since one brand can have &lt;em&gt;many&lt;/em&gt; products, we need to set up the reverse relationship explicitly on the &lt;strong&gt;branding&lt;/strong&gt; collection.&lt;/p&gt;
&lt;p&gt;Steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create Field → Relational category → &lt;strong&gt;One to Many&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Key: &lt;code&gt;products&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Related Collection: &lt;code&gt;products&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Foreign Key: &lt;code&gt;brand_id&lt;/code&gt; — the field we just created on the products side&lt;/li&gt;
&lt;li&gt;De-select &lt;strong&gt;Enable Create Button&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;seeing-the-relationship-in-action&quot;&gt;Seeing the Relationship in Action&lt;/h3&gt;
&lt;p&gt;Go back into your &lt;strong&gt;products&lt;/strong&gt; collection and assign a brand to each product. Make sure at least two products share the same brand so the relationship is visible.&lt;/p&gt;
&lt;p&gt;You'll notice each product can only belong to &lt;strong&gt;one&lt;/strong&gt; brand — that's the M2O relationship at work.&lt;/p&gt;
&lt;p&gt;Now open the &lt;strong&gt;branding&lt;/strong&gt; collection and click into one of your brands. You'll see the products you just assigned show up automatically — you didn't have to add them manually. Directus handled that because of the relationship you configured.&lt;/p&gt;
&lt;h3 id=&quot;many-to-many-m2m-products-tags&quot;&gt;Many to Many (M2M): Products ↔ Tags&lt;/h3&gt;
&lt;p&gt;M2M relationships work differently — both sides can have many related records.&lt;/p&gt;
&lt;p&gt;A common real-world example: &lt;strong&gt;tags&lt;/strong&gt; on products, used to improve SEO. A single product can have multiple tags, and a single tag can apply to many products.&lt;/p&gt;
&lt;h4 id=&quot;create-the-tags-collection&quot;&gt;Create the Tags Collection&lt;/h4&gt;
&lt;p&gt;Create a new collection called &lt;strong&gt;tags&lt;/strong&gt;, with the same two fields as before:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Field &lt;/th&gt;&lt;th&gt;Type&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;name&lt;/code&gt; &lt;/td&gt;&lt;td&gt;String&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;description&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Text (textarea)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Add three or four tags before moving on.&lt;/p&gt;
&lt;h4 id=&quot;wire-up-the-m2m-relationship&quot;&gt;Wire Up the M2M Relationship&lt;/h4&gt;
&lt;p&gt;Back in the &lt;strong&gt;products&lt;/strong&gt; collection:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create Field → Relational category → &lt;strong&gt;Many to Many&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Key: &lt;code&gt;tags&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Related Collection: &lt;code&gt;tags&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;De-select &lt;strong&gt;Enable Create Button&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do not save yet&lt;/strong&gt; — scroll down and click &lt;strong&gt;&quot;Continue in Advanced Field Creation Mode&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;In the sidebar, click &lt;strong&gt;Relationship&lt;/strong&gt;, then find &lt;strong&gt;Corresponding Field&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Check &lt;strong&gt;&quot;Create Field&quot;&lt;/strong&gt; — under Field Name, you should see &lt;code&gt;products&lt;/code&gt; appear&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Why the advanced flow?&lt;/strong&gt; Under Corresponding Field, &lt;code&gt;products&lt;/code&gt; appears automatically — that's Directus telling you it's about to create the matching relational field back on the &lt;strong&gt;tags&lt;/strong&gt; collection. Unlike the M2O/O2M setup, you don't have to manually configure both sides — Directus builds the reverse relationship for you.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, when you add a tag to a product, Directus automatically links that product under the corresponding tag in the &lt;strong&gt;tags&lt;/strong&gt; collection — no manual syncing required.&lt;/p&gt;
&lt;h3 id=&quot;summary-3&quot;&gt;Summary&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Relationship &lt;/th&gt;&lt;th&gt;Direction &lt;/th&gt;&lt;th&gt;Example&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;M2O &lt;/td&gt;&lt;td&gt;Many records → one related record &lt;/td&gt;&lt;td&gt;Products → Branding&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;O2M &lt;/td&gt;&lt;td&gt;One record → many related records &lt;/td&gt;&lt;td&gt;Branding → Products&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;M2M &lt;/td&gt;&lt;td&gt;Many ↔ many, both directions &lt;/td&gt;&lt;td&gt;Products ↔ Tags&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Relationships are one of those Directus features that make a lot more sense once you've built one yourself than they do reading about them — so I'd recommend replicating this setup in your own instance before moving on.&lt;/p&gt;
&lt;p&gt;That wraps up part two. In part three, we'll cover Directus &lt;strong&gt;permissions&lt;/strong&gt; — how to control exactly who can see and edit your data.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Found this useful? Follow for more Directus and TanStack Start tutorials, or check out the video version on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=7SNrBzaxreg&amp;amp;t=10s&quot;&gt;YouTube&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24803/directus-basics-part-2-understanding-relationships-m2o-o2m-m2m</guid>
<pubDate>Tue, 18 Aug 2026 00:38:07 +0000</pubDate>
</item>
<item>
<title>Directus Collections Explained (Fields, Types &amp; Setup) — Directus Basics Part 1</title>
<link>https://coderlegion.com/24271/directus-collections-explained-fields-types-setup-directus-basics-part-1</link>
<description>&lt;blockquote&gt;&lt;p&gt;This is Part 1 of a three-part series on Directus fundamentals: &lt;strong&gt;Collections&lt;/strong&gt; (this post), &lt;strong&gt;Relationships&lt;/strong&gt;, and &lt;strong&gt;Permissions&lt;/strong&gt;. New to Directus? Start here.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=tJGuxqhv2SY&amp;amp;t=2s&quot;&gt;Youtube Video - Directus Collections&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Collections are the bread and butter of the Directus Data Studio. According to &lt;a rel=&quot;nofollow&quot; href=&quot;https://directus.com/docs&quot;&gt;Directus's own docs&lt;/a&gt;, collections are database tables — with extra metadata and configuration layered on top by Directus.&lt;/p&gt;
&lt;p&gt;The part that makes this genuinely beginner-friendly: you never write a single line of SQL. The Data Studio handles all of that for you. Let's build one.&lt;/p&gt;
&lt;h3 id=&quot;two-ways-to-create-a-collection&quot;&gt;Two Ways to Create a Collection&lt;/h3&gt;
&lt;p&gt;There are two paths to collection creation inside the Data Studio.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option 1:&lt;/strong&gt; Log into your Data Studio and look at the sidebar for the icon that looks like a 3D box — that's &lt;strong&gt;Content&lt;/strong&gt;. Click it, and on the main screen you'll see &lt;strong&gt;Create Collection&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option 2:&lt;/strong&gt; Scroll down the sidebar to the gear icon — &lt;strong&gt;Settings&lt;/strong&gt;. Click in, and at the top you'll find &lt;strong&gt;Data Model&lt;/strong&gt;. Same deal from there — &lt;strong&gt;Create Collection&lt;/strong&gt; is right on the main screen.&lt;/p&gt;
&lt;p&gt;Either path lands you in the same place.&lt;/p&gt;
&lt;h3 id=&quot;creating-a-collection&quot;&gt;Creating a Collection&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;Create Collection&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Name it something intuitive — it should describe exactly what data you're storing. For this example, we're creating a collection called &lt;strong&gt;products&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Ignore &lt;strong&gt;Singleton&lt;/strong&gt; for now (more on this below)&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Type&lt;/strong&gt; dropdown, select &lt;strong&gt;Generated UUID&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Next&lt;/strong&gt;, select all the boxes offered, and click &lt;strong&gt;Finish Setup&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What is Singleton?&lt;/strong&gt; You'd only check this if the collection will ever hold exactly one item — not the case here, since we're storing many products.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is the Generated UUID?&lt;/strong&gt; A unique ID automatically assigned to every item in the collection, so you never have to manage IDs manually.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Once created, you land on the collection's screen, ready to build its data structure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bonus:&lt;/strong&gt; under &lt;strong&gt;Collection Setup&lt;/strong&gt;, you can customize how the collection looks — a color, an icon, and a short note describing its purpose. Small detail, but it pays off once your Directus project has a dozen collections in it.&lt;/p&gt;
&lt;h3 id=&quot;creating-fields-within-a-collection&quot;&gt;Creating Fields Within a Collection&lt;/h3&gt;
&lt;p&gt;Fields represent the actual data you're storing — in this case, the data that describes a single product. Every item in the collection shares this same structure, just with different values. Here's the full field breakdown for a &lt;strong&gt;products&lt;/strong&gt; collection:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Field         &lt;/th&gt;&lt;th&gt;Type            &lt;/th&gt;&lt;th&gt;Notes&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;name&lt;/code&gt;        &lt;/td&gt;&lt;td&gt;Input (string)  &lt;/td&gt;&lt;td&gt;The product's name&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;brief&lt;/code&gt;       &lt;/td&gt;&lt;td&gt;Textarea        &lt;/td&gt;&lt;td&gt;Short description — more room than Input, no special formatting needed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;description&lt;/code&gt; &lt;/td&gt;&lt;td&gt;WYSIWYG         &lt;/td&gt;&lt;td&gt;Full rich-text formatting — paragraphs, bullet lists, H1–H4 headings&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;price&lt;/code&gt;       &lt;/td&gt;&lt;td&gt;Input (decimal) &lt;/td&gt;&lt;td&gt;See Precision &amp;amp; Scale below&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;sale&lt;/code&gt;        &lt;/td&gt;&lt;td&gt;Input (decimal) &lt;/td&gt;&lt;td&gt;Same setup as &lt;code&gt;price&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;isOnSale&lt;/code&gt;    &lt;/td&gt;&lt;td&gt;Boolean         &lt;/td&gt;&lt;td&gt;Toggle the front end can check to decide which price to show&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;category&lt;/code&gt;    &lt;/td&gt;&lt;td&gt;Dropdown        &lt;/td&gt;&lt;td&gt;Product type — e.g. jersey, pants&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;quantity&lt;/code&gt;    &lt;/td&gt;&lt;td&gt;Input (integer) &lt;/td&gt;&lt;td&gt;Minimum value of 1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;image&lt;/code&gt;       &lt;/td&gt;&lt;td&gt;File            &lt;/td&gt;&lt;td&gt;Product photo&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;setting-up-the-price-field-precision-amp-scale-&quot;&gt;Setting Up the Price Field (Precision &amp;amp; Scale)&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;price&lt;/code&gt; field needs a bit of extra configuration. Instead of clicking &lt;strong&gt;Save&lt;/strong&gt; right away, look just below the Save button for &lt;strong&gt;Continue in Advanced Field Creation Mode&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In there, you'll find &lt;strong&gt;Precision&lt;/strong&gt; and &lt;strong&gt;Scale&lt;/strong&gt;, defaulting to &lt;code&gt;10&lt;/code&gt; and &lt;code&gt;5&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Precision&lt;/strong&gt; — the total number of digits the field accepts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scale&lt;/strong&gt; — how many of those digits sit after the decimal point&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Left at the defaults, you'd get something like &lt;code&gt;12345.67890&lt;/code&gt;. For a price field, that's overkill — we want 7 digits total, 2 of them after the decimal point. So: &lt;strong&gt;Precision = 7&lt;/strong&gt;, &lt;strong&gt;Scale = 2&lt;/strong&gt;. Adjust to fit your own use case if you need more decimal precision.&lt;/p&gt;
&lt;p&gt;Build &lt;code&gt;sale&lt;/code&gt; the exact same way.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note on relational fields:&lt;/strong&gt; collection relationships (linking one collection to another) are deliberately left out here — that's a big enough topic to earn its own post, and it's exactly what's coming next in this series.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;adding-your-data&quot;&gt;Adding Your Data&lt;/h3&gt;
&lt;p&gt;Head to the sidebar and click the box icon — &lt;strong&gt;Content&lt;/strong&gt;. You'll see your &lt;strong&gt;Products&lt;/strong&gt; collection listed. Click &lt;strong&gt;Create Item&lt;/strong&gt;, and fill in the fields with real data.&lt;/p&gt;
&lt;p&gt;For this example, add a couple of items — a jersey and a pair of jeans — to see the collection actually holding data.&lt;/p&gt;
&lt;h3 id=&quot;summary-4&quot;&gt;Summary&lt;/h3&gt;
&lt;p&gt;That's the foundation of Directus collections: the core field types, how to configure them, and how to get real data in. Next up in this series: &lt;strong&gt;relationships&lt;/strong&gt; — how collections connect to each other, which is where Directus really starts to shine.&lt;/p&gt;
&lt;p&gt;Questions or stuck on something? Drop a comment below.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24271/directus-collections-explained-fields-types-setup-directus-basics-part-1</guid>
<pubDate>Fri, 07 Aug 2026 21:18:22 +0000</pubDate>
</item>
<item>
<title>Directus + Coolify: Should You Decouple Postgres &amp; Redis?</title>
<link>https://coderlegion.com/24235/directus-coolify-should-you-decouple-postgres-redis</link>
<description>&lt;blockquote&gt;&lt;p&gt;This is Part 2 of the Directus + Coolify series. If you're new here, start with &lt;strong&gt;&quot;Secure Your VPS Before Hackers Do&quot;&lt;/strong&gt; and the first Directus + Coolify post — the bundled, single-Compose-file setup — before following along with this one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;introduction-1&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;In the first method, we coupled all of the services into one stack using a single Docker Compose file. The network between all services was created automatically, and we didn't have to start them up individually — which removes the risk of a race condition if service startup isn't handled properly.&lt;/p&gt;
&lt;p&gt;If you're running a single app, that's genuinely the recommended way to set up Directus on a Coolify-managed VPS. Going in, I assumed there were several good reasons to split the services apart instead — more control over backups, monitoring, restarts, that kind of thing. So before recommending decoupling, I actually tested each of those assumptions on a live Coolify instance.&lt;/p&gt;
&lt;p&gt;Most of them turned out to be wrong.&lt;/p&gt;
&lt;h3 id=&quot;myth-1-restarting-directus-restarts-the-whole-stack&quot;&gt;Myth 1: Restarting Directus Restarts the Whole Stack&lt;/h3&gt;
&lt;p&gt;I expected that restarting Directus inside the bundled Compose file would restart Redis and Postgres along with it. It doesn't. Coolify lets you restart each service in the stack independently — Directus, Database, and Cache each get their own &lt;strong&gt;Restart&lt;/strong&gt; button, right there in the same view. No decoupling needed for this one.&lt;/p&gt;
&lt;h3 id=&quot;myth-2-you-need-a-separate-database-resource-for-s3-backups&quot;&gt;Myth 2: You Need a Separate Database Resource for S3 Backups&lt;/h3&gt;
&lt;p&gt;Same story. Even with Postgres bundled inside the Directus Compose file, Coolify still gives it its own dedicated &lt;strong&gt;Backups&lt;/strong&gt; option, S3 included. This isn't a separate-resource-only feature.&lt;/p&gt;
&lt;h3 id=&quot;myth-3-scheduled-tasks-require-separate-services&quot;&gt;Myth 3: Scheduled Tasks Require Separate Services&lt;/h3&gt;
&lt;p&gt;Also not true. Coolify exposes a &lt;strong&gt;Scheduled Tasks&lt;/strong&gt; tab per service, even inside a single bundled stack — complete with a &lt;strong&gt;Container name&lt;/strong&gt; dropdown letting you target the cron job at just the database, or just Directus, without splitting anything apart.&lt;/p&gt;
&lt;h3 id=&quot;what-actually-holds-up&quot;&gt;What Actually Holds Up&lt;/h3&gt;
&lt;p&gt;Two things survived testing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First: metrics.&lt;/strong&gt; This one's confirmed directly in Coolify's own documentation — CPU and memory metrics collection is explicitly not available for Docker Compose–based deployments. If you want to see per-container resource usage through Coolify's built-in monitoring, the service needs to be created as its own standalone resource, not bundled inside a Compose file. This is a real, documented limitation of the bundled approach.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second: sharing a database across multiple apps.&lt;/strong&gt; This one isn't a Coolify feature at all — it's just how Docker networking works. A database defined inside one app's Compose file lives on that stack's own private network by default. A second, completely separate application can't reach it without deliberately bridging the two networks. If you've got a desktop app and a mobile app that both need to talk to the same Postgres instance, that database needs to exist as its own standalone resource from the start — it can't stay tucked inside one app's Compose file.&lt;/p&gt;
&lt;p&gt;So this really comes down to two reasons to decouple, not five — one a genuine Coolify limitation, the other a structural fact about Docker networking. Let's set both of those up properly.&lt;/p&gt;
&lt;h3 id=&quot;adding-the-resources&quot;&gt;Adding the Resources&lt;/h3&gt;
&lt;h4 id=&quot;adding-the-postgresql-database&quot;&gt;Adding the PostgreSQL Database&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dashboard → Add Project&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;+ Add Resource&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Databases → PostgreSQL → Supabase PostgreSQL (with extensions)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Change the name to something human-friendly&lt;/li&gt;
&lt;li&gt;Copy your username and password and save them somewhere — you'll need them shortly&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Start&lt;/strong&gt;, and wait for the database to spin up (this can take a little while)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once it's up, the status should read &lt;strong&gt;&quot;Running (Healthy).&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Enable metrics while you're here.&lt;/strong&gt; In the sidebar, go to &lt;strong&gt;Servers → localhost → Metrics&lt;/strong&gt;, and enable metrics. Back in your project, under &lt;strong&gt;Databases&lt;/strong&gt;, click your Postgres database, then &lt;strong&gt;Metrics&lt;/strong&gt; — you should now see live CPU/memory usage for it. This is the exact capability that isn't available on a bundled Compose deployment.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;adding-redis-cache&quot;&gt;Adding Redis Cache&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dashboard → + Add Resource&lt;/strong&gt; (from the project itself)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Databases → Redis&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Rename it to something more convenient&lt;/li&gt;
&lt;li&gt;Copy the Redis connection URL — you'll need it shortly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt;, then &lt;strong&gt;Start&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once both are running, SSH into your VPS and run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to confirm both containers are up.&lt;/p&gt;
&lt;h4 id=&quot;the-docker-compose-config-for-directus&quot;&gt;The Docker Compose Config for Directus&lt;/h4&gt;
&lt;p&gt;This is what goes into the empty Compose file for the Directus resource:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  directus:
    image: 'directus/directus:12.2.0'
    ports:
      - '8055:8055'
    volumes:
      - './uploads:/directus/uploads'
      - './extensions:/directus/extensions'
    healthcheck:
      test:
        - CMD-SHELL
        - 'wget --spider -q http://127.0.0.1:8055/server/ping || exit 1'
      interval: 10s
      timeout: 5s
      retries: 5
      start_interval: 5s
      start_period: 30s
    environment:
      SECRET: secretstring
      MARKETPLACE_TRUST: all
      DB_CLIENT: pg
      DB_HOST:
      DB_PORT: '5432'
      DB_DATABASE: postgres
      DB_USER: postgres
      DB_PASSWORD:
      CACHE_ENABLED: 'true'
      CACHE_STORE: redis
      CACHE_AUTO_PURGE: 'true'
      REDIS:
      ADMIN_EMAIL: *Emails are not allowed*
      ADMIN_PASSWORD: '1234567890'
      CORS_ENABLED: 'true'
      CORS_ORIGIN: 'true'
      CORS_CREDENTIALS: 'true'
      PUBLIC_URL:
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;Notice the healthcheck already uses &lt;code&gt;127.0.0.1&lt;/code&gt; instead of &lt;code&gt;localhost&lt;/code&gt; — that's the fix from Part 1. Carrying it forward here saves you from hitting the exact same &quot;unhealthy&quot; bug all over again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;adding-directus&quot;&gt;Adding Directus&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dashboard → + Add Resource&lt;/strong&gt; (from the project itself)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Applications → Docker Compose Empty&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Paste in the Compose configuration above&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network → Connect To Predefined Network&lt;/strong&gt; → check the box&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Services → Directus service → Settings&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Add your Directus subdomain — remember to use &lt;code&gt;https://&lt;/code&gt; (e.g. &lt;code&gt;https://directus.yourdomain.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;connecting-the-services-together&quot;&gt;Connecting the Services Together&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Variable &lt;/th&gt;&lt;th&gt;What it is&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;SECRET&lt;/code&gt; &lt;/td&gt;&lt;td&gt;A long, unguessable random string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;DB_HOST&lt;/code&gt; &lt;/td&gt;&lt;td&gt;The name of your Postgres container — run &lt;code&gt;docker ps&lt;/code&gt; on your VPS to find it&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;DB_DATABASE&lt;/code&gt; &lt;/td&gt;&lt;td&gt;The name of the database on your Postgres server (Coolify's default is usually &lt;code&gt;postgres&lt;/code&gt;, but confirm it against your Postgres service)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;DB_USER&lt;/code&gt; &lt;/td&gt;&lt;td&gt;The database username, from the Postgres service you created earlier&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;DB_PASSWORD&lt;/code&gt; &lt;/td&gt;&lt;td&gt;The password from that same Postgres service&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;REDIS&lt;/code&gt; &lt;/td&gt;&lt;td&gt;The Redis connection URL from the Redis service you created earlier&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;PUBLIC_URL&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Your Directus subdomain&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Redis URL gotcha:&lt;/strong&gt; the connection URL follows the format &lt;code&gt;redis://username:password@host:port&lt;/code&gt;. Coolify's generated URL includes the username you set when creating the Redis resource — in my case, that username was also &lt;code&gt;redis&lt;/code&gt;, so the URL looked like &lt;code&gt;redis://redis:somelongvariable...&lt;/code&gt;. Directus doesn't need the username here, just the password, so strip that segment out: &lt;code&gt;redis://:somelongvariable...&lt;/code&gt;. If you used a different username when creating your Redis resource, remove &lt;em&gt;that&lt;/em&gt; value instead — not literally the word &quot;redis.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;setting-up-environment-variables&quot;&gt;Setting Up Environment Variables&lt;/h3&gt;
&lt;p&gt;Rather than hardcoding any of this directly into the Compose file, move it into Directus's environment variables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dashboard → Projects → Services / Directus&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment Variables&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+ Add&lt;/strong&gt;, enter the variable name in all caps, and its value&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Repeat for: &lt;code&gt;SECRET&lt;/code&gt;, &lt;code&gt;DB_HOST&lt;/code&gt;, &lt;code&gt;DB_DATABASE&lt;/code&gt;, &lt;code&gt;DB_USER&lt;/code&gt;, &lt;code&gt;DB_PASSWORD&lt;/code&gt;, &lt;code&gt;ADMIN_EMAIL&lt;/code&gt;, &lt;code&gt;ADMIN_PASSWORD&lt;/code&gt;, and &lt;code&gt;REDIS&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;updating-the-compose-file&quot;&gt;Updating the Compose File&lt;/h4&gt;
&lt;p&gt;Now reference those variables instead of the raw values:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;SECRET: '${SECRET}'
DB_HOST: '${DB_HOST}'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...and so on for each variable. Then:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restart&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;launching-directus&quot;&gt;Launching Directus&lt;/h3&gt;
&lt;p&gt;Paste your Directus subdomain into the browser, and you should land on your running Directus instance. You can also click &lt;strong&gt;Links&lt;/strong&gt; on the Directus service, then click the subdomain — it'll take you straight to your Directus Studio login.&lt;/p&gt;
&lt;p&gt;Same as the last video: sign up for your free license, which arrives by email, and paste it into your Directus instance to unlock everything.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Any questions or hit a different result testing any of these yourself? Drop it in the comments — I'm genuinely curious whether this holds up across different Coolify versions and setups.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24235/directus-coolify-should-you-decouple-postgres-redis</guid>
<pubDate>Thu, 06 Aug 2026 21:39:55 +0000</pubDate>
</item>
<item>
<title>Setting Up Directus on a Coolify VPS (And Fixing the &quot;Unhealthy&quot; Error)</title>
<link>https://coderlegion.com/23995/setting-up-directus-on-a-coolify-vps-and-fixing-the-unhealthy-error</link>
<description>&lt;h3 id=&quot;introduction-2&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Directus is a backend for developers. It can connect to any SQL database and asset storage, and it provides developer tooling — which the Directus team calls the &quot;Data Engine&quot; — alongside a web application that lets both developers and non-developers manipulate data and assets through a no-code interface called the &quot;Data Studio.&quot;&lt;/p&gt;
&lt;p&gt;Directus also offers granular access control, meaning end users can only see, manipulate, and interact with the data allowed by their role and access policy — enforced consistently by both the engine and the studio. It's extensible through its own marketplace, and it's completely free, with a free license available.&lt;/p&gt;
&lt;p&gt;Directus is also known as a &lt;strong&gt;headless CMS&lt;/strong&gt; — meaning it doesn't ship with a front end attached out of the box, the way WordPress traditionally did. You can pair it with whichever front end you prefer; TanStack Start is my front end of choice.&lt;/p&gt;
&lt;p&gt;In this post, I'll self-host Directus on my Coolify-managed VPS, walk through both setup methods, and — since nothing ever works perfectly on the first try — fix the actual errors I hit along the way.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;📌 &lt;strong&gt;New here?&lt;/strong&gt; I'd recommend checking out my earlier posts where I set up the VPS itself and installed Coolify on it, before continuing with this one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;let-s-get-started&quot;&gt;Let's Get Started&lt;/h3&gt;
&lt;p&gt;There are two ways to set up Directus on a Coolify-managed VPS. I'll cover the first one here.&lt;/p&gt;
&lt;h4 id=&quot;1-log-in-to-your-coolify-dashboard&quot;&gt;1. Log in to your Coolify Dashboard&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;In the sidebar menu, go to &lt;strong&gt;Projects → +Add&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Give your project a name and description, and press continue&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-add-a-resource&quot;&gt;2. Add a Resource&lt;/h4&gt;
&lt;p&gt;After naming your project, you should be redirected to the project's page, where you can add a resource. If that redirect doesn't happen, click &lt;strong&gt;Projects&lt;/strong&gt; in the sidebar, then click into your project, and you should see &lt;strong&gt;+Add Resource&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;+Add Resource&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Under Applications, select &lt;strong&gt;Docker Compose Empty&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;A blank Docker Compose file will open&lt;/li&gt;
&lt;li&gt;Go to &lt;a rel=&quot;nofollow&quot; href=&quot;https://directus.com&quot;&gt;directus.com&lt;/a&gt; → &lt;strong&gt;Docs&lt;/strong&gt; → &lt;strong&gt;Hosting&lt;/strong&gt; → &lt;strong&gt;Deployment&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Scroll down until you find the example Docker Compose YAML file, and copy it&lt;/li&gt;
&lt;li&gt;Paste it into your blank Coolify Compose file&lt;/li&gt;
&lt;li&gt;Find the &lt;code&gt;directus:&lt;/code&gt; service, and under it, &lt;code&gt;image:&lt;/code&gt; — update this to the version you want. As of this post, the latest version is &lt;code&gt;12.2.0&lt;/code&gt;:&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;image: directus/directus:12.2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Scroll to the end and set your own admin email and password — these become your Data Studio login credentials&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt; at the top&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-configure-the-service&quot;&gt;3. Configure the Service&lt;/h4&gt;
&lt;p&gt;You should now be on the &lt;strong&gt;Configuration&lt;/strong&gt; screen, with a &lt;strong&gt;Deploy&lt;/strong&gt; button (yellow outline arrow) in the top right.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Under &lt;strong&gt;Network&lt;/strong&gt;, check the box&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Services&lt;/strong&gt;, click &lt;strong&gt;Settings&lt;/strong&gt; on the &lt;code&gt;directus&lt;/code&gt; tab&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Domain&lt;/strong&gt; field, enter your subdomain — e.g. &lt;code&gt;https://directus.yourdomain.com&lt;/code&gt;. &lt;strong&gt;Use &lt;code&gt;https&lt;/code&gt;&lt;/strong&gt;, so Traefik knows to issue a Let's Encrypt certificate for it&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;, then &lt;strong&gt;Back&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Edit Compose File&lt;/strong&gt; at the top, scroll down to &lt;code&gt;PUBLIC_URL&lt;/code&gt;, and set it to match your subdomain:&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;PUBLIC_URL: https://directus.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;, close the modal&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Deploy&lt;/strong&gt;, then &lt;strong&gt;Confirm&lt;/strong&gt; on the popup&lt;/li&gt;
&lt;li&gt;Wait for the deployment to finish&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;4-launch-directus&quot;&gt;4. Launch Directus&lt;/h4&gt;
&lt;p&gt;On the Configuration screen, click &lt;strong&gt;Links&lt;/strong&gt;, then click your new subdomain.&lt;/p&gt;
&lt;h3 id=&quot;the-errors&quot;&gt;The Errors&lt;/h3&gt;
&lt;p&gt;At this point, your browser will show two errors: a &lt;strong&gt;&quot;Not Secure&quot;&lt;/strong&gt; warning in the address bar, and a &lt;strong&gt;&quot;no available server&quot;&lt;/strong&gt; page.&lt;/p&gt;
&lt;p&gt;Back on the Coolify dashboard, you'll notice the Directus instance is showing as &lt;strong&gt;unhealthy&lt;/strong&gt;. Traefik will not route traffic to a service that's failing its health check — which is exactly what's happening here. So the first job is figuring out &lt;em&gt;why&lt;/em&gt; the health check is failing.&lt;/p&gt;
&lt;h3 id=&quot;diagnosing-the-problem&quot;&gt;Diagnosing the Problem&lt;/h3&gt;
&lt;p&gt;SSH into your VPS to start digging.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check your running containers:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Install &lt;code&gt;jq&lt;/code&gt;&lt;/strong&gt;, so the JSON output we're about to read is actually readable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install jq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Check the health check failure log&lt;/strong&gt; (grab your Directus container's name from the &lt;code&gt;docker ps&lt;/code&gt; output above):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker inspect --format='{{json .State.Health}}' &amp;lt;your-directus-container-name&amp;gt; | jq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;Status&quot;: &quot;unhealthy&quot;,
  &quot;FailingStreak&quot;: 79,
  &quot;Log&quot;: [
    {
      &quot;Start&quot;: &quot;2026-07-30T18:12:21.445964902-04:00&quot;,
      &quot;End&quot;: &quot;2026-07-30T18:12:21.502447668-04:00&quot;,
      &quot;ExitCode&quot;: 1,
      &quot;Output&quot;: &quot;wget: can't connect to remote host: Connection refused\n&quot;
    },
    {
      &quot;Start&quot;: &quot;2026-07-30T18:12:31.505411263-04:00&quot;,
      &quot;End&quot;: &quot;2026-07-30T18:12:31.556307544-04:00&quot;,
      &quot;ExitCode&quot;: 1,
      &quot;Output&quot;: &quot;wget: can't connect to remote host: Connection refused\n&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first clue: &lt;code&gt;&quot;wget: can't connect to remote host: Connection refused&quot;&lt;/code&gt;. Whatever endpoint the health check is trying to reach, it's being actively refused — not timing out, refused. That distinction matters, and it's the thread that leads to the actual fix.&lt;/p&gt;
&lt;h3 id=&quot;inspecting-the-docker-compose-file&quot;&gt;Inspecting the Docker Compose File&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;From the Coolify dashboard, select &lt;strong&gt;Projects&lt;/strong&gt; in the sidebar&lt;/li&gt;
&lt;li&gt;Click your project&lt;/li&gt;
&lt;li&gt;Click the service, then &lt;strong&gt;Edit Compose File&lt;/strong&gt; next to &lt;strong&gt;Service Stack&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's the relevant section:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;directus:
  image: 'directus/directus:12.2.0'
  ports:
    - '8055:8055'
  volumes:
    - './uploads:/directus/uploads'
    - './extensions:/directus/extensions'
  depends_on:
    database:
      condition: service_healthy
    cache:
      condition: service_healthy
  healthcheck:
    test:
      - CMD-SHELL
      - 'wget --spider -q http://localhost:8055/server/ping || exit 1'
    interval: 10s
    timeout: 5s
    retries: 5
    start_interval: 5s
    start_period: 30s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem is in the &lt;code&gt;wget&lt;/code&gt; line — specifically, &lt;code&gt;http://localhost:8055&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;why-localhost-breaks-this&quot;&gt;Why &lt;code&gt;localhost&lt;/code&gt; Breaks This&lt;/h4&gt;
&lt;p&gt;Inside a Linux container, &lt;code&gt;localhost&lt;/code&gt; isn't an address — it's a hostname that has to be resolved first, and it typically maps to &lt;strong&gt;two&lt;/strong&gt; addresses at once:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;127.0.0.1   localhost   # IPv4
::1         localhost   # IPv6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When &lt;code&gt;wget&lt;/code&gt; looks up &lt;code&gt;localhost&lt;/code&gt;, the system's resolver hands back both addresses, and &lt;code&gt;wget&lt;/code&gt; tries them in whatever order it receives them — which, on many Linux/Alpine base images, tends to prefer &lt;strong&gt;IPv6 first&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Checking the container's logs confirms what's actually listening:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker logs &amp;lt;your-directus-container-name&amp;gt; --tail 150
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Directus only bound to IPv4. The startup log says exactly that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Server started at http://0.0.0.0:8055
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;0.0.0.0&lt;/code&gt; means &quot;listen on every IPv4 interface&quot; — Directus never opened an IPv6 socket at all. So when &lt;code&gt;wget&lt;/code&gt; tries &lt;code&gt;::1:8055&lt;/code&gt; first, there's genuinely nothing listening there, and the OS doesn't wait around wondering — it immediately sends back a rejection (a TCP RST). That's exactly why the log shows an instant &lt;strong&gt;&quot;Connection refused&quot;&lt;/strong&gt; rather than a slow timeout. A timeout would mean something was reachable but not responding; a refusal means the OS said &quot;nothing's here&quot; right away.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;127.0.0.1&lt;/code&gt; sidesteps the whole problem, because it's already a literal IP address — no hostname lookup, no ambiguity about which protocol family to try, no chance of picking the wrong one. It goes straight to the one address where Directus is actually listening.&lt;/p&gt;
&lt;h3 id=&quot;the-fix&quot;&gt;The Fix&lt;/h3&gt;
&lt;p&gt;Change &lt;code&gt;localhost&lt;/code&gt; to &lt;code&gt;127.0.0.1&lt;/code&gt; in the health check:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;directus:
  image: 'directus/directus:12.2.0'
  ports:
    - '8055:8055'
  volumes:
    - './uploads:/directus/uploads'
    - './extensions:/directus/extensions'
  depends_on:
    database:
      condition: service_healthy
    cache:
      condition: service_healthy
  healthcheck:
    test:
      - CMD-SHELL
      - 'wget --spider -q http://127.0.0.1:8055/server/ping || exit 1'
    interval: 10s
    timeout: 5s
    retries: 5
    start_interval: 5s
    start_period: 30s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save the change, close the modal, and click &lt;strong&gt;Restart&lt;/strong&gt; in the top right. Wait for the restart to complete — it may look like nothing's happening for a bit, so give it a moment. Once it's finished, close the logs modal, and Directus should show as &lt;strong&gt;healthy&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Click &lt;strong&gt;Links&lt;/strong&gt;, then your Directus subdomain, and you should land on the Data Studio login screen.&lt;/p&gt;
&lt;h3 id=&quot;logging-in-amp-licensing&quot;&gt;Logging In &amp;amp; Licensing&lt;/h3&gt;
&lt;p&gt;Enter the credentials you set earlier in the Compose file. You'll be asked whether you have a license or want to install the Core — choose &lt;strong&gt;Core&lt;/strong&gt; for now, and follow the flow to complete sign-up.&lt;/p&gt;
&lt;p&gt;At this stage, your instance isn't fully unlocked — you'll need a license. Good news: it's completely free if your business's revenue is under $5M and your team is under 50 people.&lt;/p&gt;
&lt;p&gt;Head to &lt;a rel=&quot;nofollow&quot; href=&quot;https://directus.com/oig&quot;&gt;directus.com/oig&lt;/a&gt; to apply for your key. Once you have it, go to your Directus Studio, click the gear icon (Settings) in the sidebar, click &lt;strong&gt;License&lt;/strong&gt;, and add your key there to unlock all features. Your key is valid for a year.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Got questions, or hit a different error setting this up? Drop a comment below — happy to help troubleshoot.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23995/setting-up-directus-on-a-coolify-vps-and-fixing-the-unhealthy-error</guid>
<pubDate>Sun, 02 Aug 2026 14:42:21 +0000</pubDate>
</item>
</channel>
</rss>