<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Wade Thomas</title>
    <description>The latest articles on DEV Community by Wade Thomas (@wadethomastt).</description>
    <link>https://dev.to/wadethomastt</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3850437%2F8d98205a-0b4e-4e70-abeb-0bb759317abf.jpg</url>
      <title>DEV Community: Wade Thomas</title>
      <link>https://dev.to/wadethomastt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wadethomastt"/>
    <language>en</language>
    <item>
      <title>Ansible Installation and Configuration on Ubuntu — Automate Your Server Management</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Fri, 10 Jul 2026 04:17:07 +0000</pubDate>
      <link>https://dev.to/wadethomastt/ansible-installation-and-configuration-on-ubuntu-automate-your-server-management-1fdg</link>
      <guid>https://dev.to/wadethomastt/ansible-installation-and-configuration-on-ubuntu-automate-your-server-management-1fdg</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/1U8ID8j_gLg"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Ansible?
&lt;/h3&gt;

&lt;p&gt;Ansible is an automation language that can describe any IT environment, whether homelab or large-scale infrastructure. It is easy to learn and reads like clear documentation.&lt;/p&gt;

&lt;p&gt;If you manage multiple servers and find yourself doing the same configuration over and over — setting up SSH keys, disabling root users, configuring firewalls — Ansible can automate the entire process and dramatically increase your productivity.&lt;/p&gt;

&lt;p&gt;It only requires Ansible on the &lt;strong&gt;Control Node&lt;/strong&gt; and &lt;strong&gt;Python 3&lt;/strong&gt; on the &lt;strong&gt;Managed Node&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the Control Node?
&lt;/h3&gt;

&lt;p&gt;The system that Ansible is installed on — it controls the remote machines.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the Managed Node?
&lt;/h3&gt;

&lt;p&gt;The remote system or host that Ansible controls. Ansible is &lt;strong&gt;agentless&lt;/strong&gt;, meaning you don't need to install Ansible on managed nodes — just Python 3.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installing Ansible on Ubuntu (Control Node)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;software-properties-common
&lt;span class="nb"&gt;sudo &lt;/span&gt;add-apt-repository &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="nt"&gt;--update&lt;/span&gt; ppa:ansible/ansible
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ansible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Ensure Python 3 is installed on your remote server. Ubuntu 24.04 LTS ships with Python 3 by default.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Create the Inventory Folder and Hosts File
&lt;/h2&gt;

&lt;p&gt;The hosts file maps the remote machines you want to control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Folder structure:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ansible/&lt;br&gt;
├── inventory/&lt;br&gt;
│   └── hosts&lt;br&gt;
└── playbooks/&lt;br&gt;
└── apt-update.yml&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;inventory/hosts&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[servers]&lt;/span&gt;
&lt;span class="err"&gt;vpsServer&lt;/span&gt; &lt;span class="py"&gt;ansible_host&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10.10.100.45&lt;/span&gt;
&lt;span class="err"&gt;work-ToRule&lt;/span&gt;
&lt;span class="err"&gt;10.10.45.62&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can give hosts an alias by pairing a name with an IP address. In the example above, &lt;code&gt;vpsServer&lt;/code&gt; is an alias for &lt;code&gt;10.10.100.45&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your First Playbook — Update Ubuntu and Set Timezone
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;playbooks/apt-update.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;serial&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set system timezone to Trinidad and Tobago time&lt;/span&gt;
      &lt;span class="na"&gt;community.general.timezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;America/Port_of_Spain&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update apt cache&lt;/span&gt;
      &lt;span class="na"&gt;apt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;update_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
        &lt;span class="na"&gt;cache_valid_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upgrade all packages to the latest version&lt;/span&gt;
      &lt;span class="na"&gt;apt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;upgrade&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check if reboot is required&lt;/span&gt;
      &lt;span class="na"&gt;stat&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/run/reboot-required&lt;/span&gt;
      &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reboot_required_file&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reboot the server&lt;/span&gt;
      &lt;span class="na"&gt;reboot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Reboot&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;initiated&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;by&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Ansible&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;due&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;package&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;upgrades'&lt;/span&gt;
        &lt;span class="na"&gt;connect_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
        &lt;span class="na"&gt;reboot_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
        &lt;span class="na"&gt;pre_reboot_delay&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="na"&gt;post_reboot_delay&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reboot_required_file.stat.exists&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Breaking Down the Playbook
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hosts: '*'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Target all hosts in inventory. Use an alias, DNS name, or IP to target a single host.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;become: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Grants Ansible sudo privileges.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;serial: 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Processes servers one at a time instead of all at once.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tasks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A list of individual actions to run on the target hosts.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each task has four parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;name&lt;/code&gt;&lt;/strong&gt; — a plain-text description of what the task does&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collection&lt;/strong&gt; (&lt;code&gt;community.general&lt;/code&gt;) — the Ansible content bundle the module belongs to&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Module&lt;/strong&gt; (&lt;code&gt;timezone&lt;/code&gt;, &lt;code&gt;apt&lt;/code&gt;, &lt;code&gt;reboot&lt;/code&gt;) — the tool that executes the action&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parameters&lt;/strong&gt; (&lt;code&gt;name: America/Port_of_Spain&lt;/code&gt;) — the specific options passed to the module&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;📚 Browse all available modules and collections at &lt;a href="https://docs.ansible.com/projects/ansible/latest/collections/index.html" rel="noopener noreferrer"&gt;docs.ansible.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Running the Playbook
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1 — Test connectivity with a ping
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ANSIBLE_HOST_KEY_CHECKING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;FALSE ansible &lt;span class="nt"&gt;-i&lt;/span&gt; ./inventory/hosts vpsServer &lt;span class="nt"&gt;-m&lt;/span&gt; ping &lt;span class="nt"&gt;--user&lt;/span&gt; root &lt;span class="nt"&gt;--ask-pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ANSIBLE_HOST_KEY_CHECKING=FALSE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Skips SSH host key verification — useful for fresh servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-i ./inventory/hosts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Points to your inventory file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vpsServer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The target host alias&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-m ping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs the ping module to check connectivity and Python availability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--ask-pass&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prompts for SSH password&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once you get a green &lt;strong&gt;pong&lt;/strong&gt; response, you're ready to run the playbook.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Run the playbook
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-playbook ./playbooks/apt-update.yml &lt;span class="nt"&gt;--user&lt;/span&gt; root &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"ansible_port=22"&lt;/span&gt; &lt;span class="nt"&gt;--ask-pass&lt;/span&gt; &lt;span class="nt"&gt;--ask-become-pass&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; ./inventory/hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ansible-playbook&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs a full automation script instead of a single ad-hoc task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;./playbooks/apt-update.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Path to your playbook file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--user root&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SSH connection username&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-e "ansible_port=22"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Injects extra variable to force port 22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--ask-pass&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prompts for SSH login password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--ask-become-pass&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prompts for sudo password (redundant when logging in as root)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-i ./inventory/hosts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Points to your inventory file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Execution Flow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Ansible reads &lt;code&gt;./inventory/hosts&lt;/code&gt; to find the target server's IP&lt;/li&gt;
&lt;li&gt;Prompts for SSH password&lt;/li&gt;
&lt;li&gt;Prompts for sudo password&lt;/li&gt;
&lt;li&gt;Connects to port 22 as root&lt;/li&gt;
&lt;li&gt;Opens &lt;code&gt;apt-update.yml&lt;/code&gt; and executes each task in order&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A big shout-out to Aldo &lt;a href="https://dev.to/aldo_cve"&gt;@aldo_cve&lt;/a&gt; for recommending Ansible in a previous post — it's been a great addition to my server management workflow.&lt;/p&gt;

&lt;p&gt;I hope you found this walkthrough useful. Stay tuned for more posts where I share playbooks I find useful in my day-to-day infrastructure work.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Looking for developer templates built on TanStack, Directus, and Tailwind CSS? Check out my store 👇&lt;/em&gt;&lt;br&gt;
&lt;em&gt;🛒 &lt;a href="https://northernrangedigital.lemonsqueezy.com/" rel="noopener noreferrer"&gt;northernrangedigital.lemonsqueezy.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ansible</category>
      <category>devops</category>
      <category>ubuntu</category>
      <category>linux</category>
    </item>
    <item>
      <title>How to Secure a VPS: The Complete Ubuntu Hardening Guide</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Thu, 02 Jul 2026 05:26:20 +0000</pubDate>
      <link>https://dev.to/wadethomastt/how-to-secure-a-vps-the-complete-ubuntu-hardening-guide-40ee</link>
      <guid>https://dev.to/wadethomastt/how-to-secure-a-vps-the-complete-ubuntu-hardening-guide-40ee</guid>
      <description>&lt;p&gt;The moment a VPS gets a public IP, it's already being scanned. Automated bots start hammering the default root account with password guesses within minutes of the server going online — before you've even finished your first &lt;code&gt;apt update&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This guide walks through the baseline hardening I run on every fresh Ubuntu server before deploying anything to it: creating a proper user account, locking down root, setting up a firewall, quieting bot noise on SSH, and auto-banning repeat offenders.&lt;/p&gt;

&lt;p&gt;Prefer to follow along on video?&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/bv9OtbRqLMo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Stop Using the Root User
&lt;/h2&gt;

&lt;p&gt;Every Linux server in the world ships with an account named &lt;code&gt;root&lt;/code&gt;. That's the problem — hackers already know the username, so they only need to guess the password or find one exploit to get full control. A custom username forces an attacker to guess two unknowns instead of one.&lt;/p&gt;

&lt;p&gt;A few other reasons to move off root:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No safety net.&lt;/strong&gt; Root executes destructive commands instantly, no confirmation. Run &lt;code&gt;rm -rf /&lt;/code&gt; as root and it's gone. A standard user needs &lt;code&gt;sudo&lt;/code&gt;, which at least forces a pause and a password prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bots target root specifically.&lt;/strong&gt; The moment a VPS goes online, automated bots start brute-forcing the root account with thousands of password guesses per minute. Disable root login and that entire attack surface disappears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No accountability.&lt;/strong&gt; If more than one person has server access, a shared root login makes it impossible to tell who did what. Individual sudo accounts get logged to &lt;code&gt;/var/log/auth.log&lt;/code&gt;, so every command is tied to a specific user.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Create a sudo user
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adduser your_username
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;your_username
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-aG&lt;/code&gt; breaks down into two flags: &lt;code&gt;-G&lt;/code&gt; adds the user to the group that follows (&lt;code&gt;sudo&lt;/code&gt;), and &lt;code&gt;-a&lt;/code&gt; (append) makes sure the user is &lt;em&gt;added&lt;/em&gt; to that group rather than having all their other group memberships wiped out. Together, &lt;code&gt;-aG&lt;/code&gt; says: add this user to &lt;code&gt;sudo&lt;/code&gt;, keep everything else as-is.&lt;/p&gt;

&lt;p&gt;Verify it worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;groups &lt;/span&gt;your_username
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Lock down root
&lt;/h3&gt;

&lt;p&gt;Log out of root and back in as your new sudo user first, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Lock the root password&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;passwd &lt;span class="nt"&gt;-l&lt;/span&gt; root

&lt;span class="c"&gt;# Disable root login over SSH&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/ssh/sshd_config
&lt;span class="c"&gt;# set: PermitRootLogin no&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart sshd

&lt;span class="c"&gt;# Confirm root is locked&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;passwd &lt;span class="nt"&gt;-S&lt;/span&gt; root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Set Up a Firewall (UFW)
&lt;/h2&gt;

&lt;p&gt;A firewall closes off everything you're not explicitly using. On a fresh VPS, that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Blocking brute-force attempts on ports you don't need exposed&lt;/li&gt;
&lt;li&gt;Keeping internal-only services (databases, admin tools) off the public internet&lt;/li&gt;
&lt;li&gt;Restricting sensitive ports like SSH to specific IPs, if needed&lt;/li&gt;
&lt;li&gt;Dropping unexpected traffic, which softens basic DoS attempts&lt;/li&gt;
&lt;li&gt;Closing the door on any hidden vulnerability in something you're running&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ubuntu ships with UFW (Uncomplicated Firewall), which blocks all incoming traffic by default and only opens what you explicitly allow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Allow SSH first so you don't lock yourself out&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow OpenSSH
&lt;span class="c"&gt;# or: sudo ufw allow 22/tcp&lt;/span&gt;

&lt;span class="c"&gt;# Allow web traffic if you're hosting a site&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow http
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow https

&lt;span class="c"&gt;# Turn it on&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;

&lt;span class="c"&gt;# Check status&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw status verbose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Change the Default SSH Port
&lt;/h2&gt;

&lt;p&gt;Within minutes of going live, bots start hammering port 22 — not targeted attacks, just scripts sweeping the internet for the default SSH port. Moving to something non-standard, like 2222, makes those scanners skip right past you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/ssh/sshd_config
&lt;span class="c"&gt;# uncomment #Port 22 and change it:&lt;/span&gt;
&lt;span class="c"&gt;# Port 2222&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 2222/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth being honest about this one: security researchers call this "security through obscurity," and on its own it's a weak measure — it doesn't make the server harder to break into. What it does do is keep your auth logs from being flooded with bot noise, which makes real suspicious activity much easier to spot. Pair it with SSH keys, disabled password auth, and Fail2Ban for actual hardening.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Install Fail2Ban
&lt;/h2&gt;

&lt;p&gt;Fail2Ban watches your logs for repeated failed login attempts and temporarily bans the offending IP at the firewall level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's worth running:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free, open-source, and quick to set up&lt;/li&gt;
&lt;li&gt;Highly configurable — ban duration, whitelisted IPs, which services to watch&lt;/li&gt;
&lt;li&gt;Bans happen at the firewall, so malicious traffic doesn't eat server resources&lt;/li&gt;
&lt;li&gt;Can integrate with notifications for real-time alerts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where it falls short:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reactive, not preventive — it only acts after a set number of failed attempts have already happened&lt;/li&gt;
&lt;li&gt;Can lock out legitimate users who fat-finger a password a few times in a row&lt;/li&gt;
&lt;li&gt;Weak against distributed attacks, since it bans by IP and botnets rotate through thousands of them&lt;/li&gt;
&lt;li&gt;Vulnerable to IP spoofing&lt;/li&gt;
&lt;li&gt;Can conflict with Docker's iptables rules on a Docker host, causing bans to fail or hit the wrong container&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Install it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;fail2ban &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start fail2ban
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;fail2ban
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The &lt;code&gt;-y&lt;/code&gt; flag auto-confirms every prompt during install — only use it once you're already certain about the package.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-Up
&lt;/h2&gt;

&lt;p&gt;None of these steps make a server unbreakable on their own — a sudo user, a firewall, an obscure SSH port, and Fail2Ban are each individually beatable. Stacked together, they cut off the low-effort, automated attacks that hit every public IP within minutes of going live, and they keep your logs clean enough that you'd actually notice something that isn't normal.&lt;/p&gt;

&lt;p&gt;If you found this useful, the video above walks through each step live. Let me know in the comments if you run into anything setting this up on your own box.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What is Redis? The In-Memory Data Store That Makes Your App Faster</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Wed, 10 Jun 2026 03:44:49 +0000</pubDate>
      <link>https://dev.to/wadethomastt/what-is-redis-the-in-memory-data-store-that-makes-your-app-faster-291p</link>
      <guid>https://dev.to/wadethomastt/what-is-redis-the-in-memory-data-store-that-makes-your-app-faster-291p</guid>
      <description>&lt;p&gt;🎬 This article is a companion to my YouTube video. Watch it here:&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/e3KNJr1ATv8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this video we are going to talk about Redis — what it is, what it does, and why it is an important part of my back-end stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Redis?
&lt;/h2&gt;

&lt;p&gt;Redis is a free, open-source, in-memory data store. Unlike PostgreSQL which stores data on disk, Redis stores data entirely in memory — in RAM. This makes it extremely fast. Redis can handle millions of operations per second with sub-millisecond response times.&lt;br&gt;
Redis is most commonly used as a cache, a session store, a message broker, and a real-time data store.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Caching?
&lt;/h2&gt;

&lt;p&gt;When your application queries a database, that query takes time — it reads from disk, processes the query, and returns the result. If the same query is made thousands of times per second, you are hitting the database thousands of times unnecessarily.&lt;br&gt;
Caching solves this by storing the result of a query in memory. The first request hits the database and the result is stored in Redis. Every subsequent request gets the result from Redis — which is in memory and therefore much faster — instead of hitting the database again.&lt;br&gt;
Think of it like a shortcut. Instead of driving the long route to the database every time, you take the shortcut through Redis.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does Redis Do?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Caching
&lt;/h3&gt;

&lt;p&gt;Store frequently accessed data in memory for fast retrieval. Database query results, API responses, computed values — anything that is expensive to compute and accessed frequently is a good candidate for caching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Session Storage
&lt;/h3&gt;

&lt;p&gt;Store user session data in Redis instead of the database. Since sessions are read on every request, having them in memory is significantly faster than a database lookup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate Limiting
&lt;/h3&gt;

&lt;p&gt;Track how many requests a user or IP address has made in a given time window. Redis's atomic increment operations make it perfect for implementing rate limiting.&lt;br&gt;
Message Queues and Pub/Sub&lt;br&gt;
Redis supports publish/subscribe messaging and message queues. Applications can publish messages to a channel and subscribers receive them in real time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leaderboards and Counters
&lt;/h3&gt;

&lt;p&gt;Redis sorted sets make it trivial to implement leaderboards, counters, and real-time analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Directus Uses Redis
&lt;/h2&gt;

&lt;p&gt;Directus uses Redis for two primary purposes.&lt;br&gt;
First, as a cache layer. Directus caches API responses, schema information, and permission lookups in Redis. This dramatically reduces database load and speeds up API response times.&lt;br&gt;
Second, for synchronization across multiple Directus instances. If you run multiple instances of Directus for high availability or horizontal scaling, Redis acts as the shared cache and message bus that keeps them in sync.&lt;br&gt;
For a single Directus instance Redis is optional but recommended. For multiple instances it is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Chose Redis
&lt;/h2&gt;

&lt;p&gt;Redis is the industry standard for caching and session storage. It is fast, reliable, widely supported, and Directus has first-class support for it. Adding Redis to the stack costs very little in terms of resources but provides significant performance benefits as the application scales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Redis is a powerful in-memory data store that makes your application faster and more scalable by caching frequently accessed data and handling real-time workloads. It is a small but important piece of a production-ready back-end stack.&lt;br&gt;
In an upcoming video we will deploy Redis alongside Directus and PostgreSQL on our VPS using Coolify.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Redis Website&lt;/li&gt;
&lt;li&gt;Redis Documentation&lt;/li&gt;
&lt;li&gt;Redis GitHub&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔔 Subscribe to my YouTube channel for the full series on building a modern web app back end from scratch.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>redis</category>
      <category>caching</category>
      <category>performance</category>
    </item>
    <item>
      <title>What is PostgreSQL? The Database That Powers Modern Web Applications</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Tue, 26 May 2026 15:28:23 +0000</pubDate>
      <link>https://dev.to/wadethomastt/what-is-postgresql-the-database-that-powers-modern-web-applications-4l83</link>
      <guid>https://dev.to/wadethomastt/what-is-postgresql-the-database-that-powers-modern-web-applications-4l83</guid>
      <description>&lt;p&gt;🎬 This article is a companion to my YouTube video. Watch it here:&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/qPm6Fa2G_gQ"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this video we are going to talk about PostgreSQL — what it is, why it is one of the most popular databases in the world, and why it is the database I use to power my web applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is PostgreSQL?
&lt;/h2&gt;

&lt;p&gt;PostgreSQL — often called Postgres — is a free, open-source relational database management system. It has been in active development for over 35 years and is widely considered one of the most advanced, stable and feature-rich databases available.&lt;br&gt;
A relational database stores data in tables — rows and columns — and uses SQL to query and manipulate that data. If you have ever worked with a spreadsheet, you already understand the basic concept.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PostgreSQL?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ACID compliance
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is fully ACID compliant — Atomicity, Consistency, Isolation, Durability. Transactions either complete fully or not at all. If something goes wrong mid-transaction, the database rolls back to the previous state. For financial data, orders, user accounts — anything where data integrity matters — this is critical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced data types
&lt;/h3&gt;

&lt;p&gt;PostgreSQL supports JSON and JSONB for document storage, arrays, UUID, geometric types, full-text search and more. You get the flexibility of a document database with the reliability of a relational database.&lt;br&gt;
Excellent performance&lt;br&gt;
PostgreSQL handles complex queries, large datasets and high concurrency extremely well. It has a sophisticated query planner and optimizer that makes even complex joins and aggregations fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extensibility
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is highly extensible. PostGIS for geospatial data, pgvector for AI embeddings, and TimescaleDB for time series data are just a few examples of powerful extensions available.&lt;br&gt;
Open source and free&lt;br&gt;
PostgreSQL is completely free and open source with no licensing costs. There is no enterprise tier required to access advanced features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Widely supported
&lt;/h3&gt;

&lt;p&gt;Almost every major framework, ORM, and tool supports PostgreSQL. Directus, Prisma, Drizzle, Sequelize, Django, Rails — they all work with PostgreSQL out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  PostgreSQL vs MySQL
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is more standards compliant and supports more advanced features out of the box. MySQL is slightly simpler to set up and has historically been faster for simple read-heavy workloads. For modern web applications with complex data requirements, PostgreSQL is generally the better choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Chose PostgreSQL
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is the database that Directus recommends and works best with. It gives me full ACID compliance, advanced data types, and excellent performance — everything I need for production web applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is a battle-tested, feature-rich, open-source relational database that powers some of the world's largest applications. For modern web development it is one of the safest and most capable choices available.&lt;br&gt;
In an upcoming video we will deploy PostgreSQL alongside Directus on our VPS using Coolify.&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;• PostgreSQL Website
• PostgreSQL Documentation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;🔔 Subscribe to my YouTube channel for the full series on building a modern web app back end from scratch.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>backenddevelopment</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What is Directus? The Headless CMS That Sits On Your Own Database</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Sun, 17 May 2026 22:17:59 +0000</pubDate>
      <link>https://dev.to/wadethomastt/what-is-directus-the-headless-cms-that-sits-on-your-own-database-10d</link>
      <guid>https://dev.to/wadethomastt/what-is-directus-the-headless-cms-that-sits-on-your-own-database-10d</guid>
      <description>&lt;p&gt;🎬 This article is a companion to my YouTube video. Watch it here:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/83OJERORAj8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Before we get into setting up Coolify, I want to make sure you understand the tools we will be deploying. In this video we are going to talk about Directus — what it is, what it does, and why I chose it as the backbone of my back-end stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Directus?
&lt;/h2&gt;

&lt;p&gt;Directus is a free, open-source, headless CMS and data platform. But before we go any further, let me explain what headless CMS means.&lt;/p&gt;

&lt;p&gt;A traditional CMS — like WordPress — couples the content management system with the front end. The way your content is stored and the way it is displayed are tightly linked. You are locked into how WordPress presents your content.&lt;/p&gt;

&lt;p&gt;A headless CMS separates the two. Directus manages and stores your data, and exposes it through a REST API or GraphQL endpoint. Your front end — whether it is a React app, a mobile app, or anything else — consumes that API and decides how to display the content. The CMS has no head — meaning no fixed front end — hence the name headless.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes Directus Different?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  It works with your existing database
&lt;/h3&gt;

&lt;p&gt;Most headless CMS platforms use their own proprietary data storage. Directus sits on top of a standard relational database — PostgreSQL, MySQL, SQLite and more. Your data is stored in plain database tables that you own and can access directly. You are never locked into a proprietary format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto-generated API
&lt;/h3&gt;

&lt;p&gt;When you create a collection in Directus — think of a collection like a database table — it automatically generates a full REST API and GraphQL endpoint for that collection. No code required. You get full CRUD operations out of the box — create, read, update and delete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Powerful admin dashboard
&lt;/h3&gt;

&lt;p&gt;Directus comes with a beautiful, fully featured admin dashboard out of the box. Your clients or content editors can manage content without ever touching code. You can customize the dashboard with custom fields, relationships, file uploads, translations and more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Role-based access control
&lt;/h3&gt;

&lt;p&gt;Directus has a very granular permissions system. You can control exactly who can read, create, update or delete data at the collection level, the field level, and even the row level. This makes it suitable for multi-tenant applications and complex permission requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  File management
&lt;/h3&gt;

&lt;p&gt;Directus includes a full file and asset management system. You can upload images, videos, documents and more. It supports on-the-fly image transformations — resize, crop, format conversion — all through URL parameters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flows — built-in automation
&lt;/h3&gt;

&lt;p&gt;Directus has a built-in automation system called Flows. You can build workflows triggered by events — like sending an email when a new order is created, or updating a related record when a status changes — all without writing code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open source and self-hostable
&lt;/h3&gt;

&lt;p&gt;Directus is completely open source. You can self-host it on your own server, which means your data stays on your infrastructure. There is also a cloud hosted option if you prefer a managed solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Chose Directus
&lt;/h2&gt;

&lt;p&gt;I chose Directus for several reasons.&lt;/p&gt;

&lt;p&gt;First, it works with PostgreSQL out of the box. I wanted a standard relational database that I own and control, not a proprietary data store.&lt;/p&gt;

&lt;p&gt;Second, the auto-generated API saves me an enormous amount of time. Instead of building CRUD endpoints for every collection, Directus handles that automatically. I focus on building features, not boilerplate API code.&lt;/p&gt;

&lt;p&gt;Third, the admin dashboard is genuinely excellent. My clients can manage their own content without any technical knowledge. I do not have to build a custom admin interface for every project.&lt;/p&gt;

&lt;p&gt;Fourth, it is self-hostable. My data stays on my server. I control the infrastructure, the costs, and the data.&lt;/p&gt;




&lt;h2&gt;
  
  
  What are the Limitations?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not a traditional backend framework&lt;/strong&gt; — complex business logic may require supplementing with custom code or choosing a different solution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Licensing costs at scale&lt;/strong&gt; — Directus is free and open source for projects generating up to &lt;strong&gt;$5 million USD&lt;/strong&gt; in annual revenue. Beyond that threshold a commercial license is required. For the vast majority of startups, small teams and indie developers this limit will never be reached, making it effectively free for most use cases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Can be overkill for simple projects&lt;/strong&gt; — a basic blog may not need all of Directus's features.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Directus is a powerful, flexible, open-source headless CMS that sits on top of your own database and gives you a full API and admin dashboard out of the box. For developers building modern web applications who want to own their data and move fast without writing boilerplate, it is one of the best tools available.&lt;/p&gt;

&lt;p&gt;In an upcoming video we will deploy Directus on our VPS using Coolify and connect it to our TanStack Start front end.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://directus.io" rel="noopener noreferrer"&gt;Directus Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.directus.io" rel="noopener noreferrer"&gt;Directus Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/directus/directus" rel="noopener noreferrer"&gt;Directus GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;🔔 Subscribe to my YouTube channel for the full series on building a modern web app back end from scratch.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>directus</category>
      <category>headlesscms</category>
      <category>opensource</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>What is Coolify? Self-Hosting with Superpowers</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Thu, 14 May 2026 03:40:29 +0000</pubDate>
      <link>https://dev.to/wadethomastt/what-is-coolify-self-hosting-with-superpowers-o01</link>
      <guid>https://dev.to/wadethomastt/what-is-coolify-self-hosting-with-superpowers-o01</guid>
      <description>&lt;p&gt;🎬 This article is a companion to my YouTube video. Watch it here:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/oFmJYMk1iCg"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the last video, we talked about the VPS and why it is a compelling option for hosting your web applications. I mentioned a tool called Coolify that makes managing a VPS significantly easier. In this video, we are going to dive deeper into what Coolify actually is, what it does, and why I think it is one of the best tools available for developers and small teams who want the power of a VPS without the complexity of managing one from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Coolify?
&lt;/h2&gt;

&lt;p&gt;Coolify is a free, open-source, self-hostable platform as a service — or PaaS. Think of it as your own personal Heroku or Render, but running on your own server. This means you own your infrastructure, your data, and your costs.&lt;/p&gt;

&lt;p&gt;The best way to understand Coolify is to compare it to the alternatives. Platforms like Heroku, Render, and Railway are fully managed PaaS solutions. They abstract away all the server complexity — you push your code and it runs. The trade-off is cost and control. As your app scales, the bills grow quickly and you have limited control over the underlying infrastructure.&lt;/p&gt;

&lt;p&gt;Coolify gives you the same developer experience — push your code and it deploys — but on a VPS that you control. You get the simplicity of a managed platform with the economics and control of a VPS.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Does Coolify Do?
&lt;/h2&gt;

&lt;p&gt;Coolify handles all the hard parts of running applications on a VPS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Git Integration
&lt;/h3&gt;

&lt;p&gt;Connect your GitHub, GitLab, or Bitbucket repository and Coolify will automatically deploy your app every time you push to your main branch. No manual deployments, no SSH commands — just push your code and it is live.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dockerized Deployments
&lt;/h3&gt;

&lt;p&gt;Every application Coolify deploys runs in a Docker container. This means your apps are isolated, portable, and consistent across environments. You do not need to know Docker deeply to use Coolify — it handles the containerization for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic HTTPS
&lt;/h3&gt;

&lt;p&gt;Coolify integrates with Let's Encrypt to automatically provision and renew SSL certificates for all your applications. Every app gets HTTPS out of the box with zero configuration on your part.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built-in Reverse Proxy
&lt;/h3&gt;

&lt;p&gt;Coolify uses Traefik as its built-in reverse proxy and web server. It automatically routes traffic to the right application based on the domain name. You can run multiple applications on the same VPS and Coolify handles the routing between them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database Management
&lt;/h3&gt;

&lt;p&gt;Coolify can deploy and manage databases alongside your applications — PostgreSQL, MySQL, MongoDB, Redis and more. You can spin up a database with a few clicks and connect it to your application without any manual configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment Variables
&lt;/h3&gt;

&lt;p&gt;Manage your environment variables securely through the Coolify dashboard. No more manually editing &lt;code&gt;.env&lt;/code&gt; files on the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring and Logs
&lt;/h3&gt;

&lt;p&gt;Coolify provides basic monitoring and real-time log streaming for all your applications directly from the dashboard. You can see what your app is doing without SSH-ing into the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backups
&lt;/h3&gt;

&lt;p&gt;Coolify supports automated database backups to S3-compatible storage. Your data is protected without any manual backup scripts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Would You Use Coolify?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  You want the economics of a VPS without the complexity
&lt;/h3&gt;

&lt;p&gt;A $6 to $10 per month VPS with Coolify can run multiple applications that would cost hundreds of dollars per month on Heroku, Render, or Railway. For a startup or indie developer this is a significant saving.&lt;/p&gt;

&lt;h3&gt;
  
  
  You want full control over your infrastructure
&lt;/h3&gt;

&lt;p&gt;With Coolify you own everything. Your data stays on your server. You choose your hosting provider. You are not locked into any platform's pricing or terms of service.&lt;/p&gt;

&lt;h3&gt;
  
  
  You want a great developer experience
&lt;/h3&gt;

&lt;p&gt;Coolify's dashboard is clean and intuitive. Deploying an application is genuinely just a few clicks. It does not feel like managing a server — it feels like using a modern PaaS.&lt;/p&gt;

&lt;h3&gt;
  
  
  You are running multiple projects
&lt;/h3&gt;

&lt;p&gt;One VPS with Coolify can host multiple applications, multiple databases, and multiple domains. Instead of paying for separate hosting for each project, you consolidate everything onto one server.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Are the Limitations?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You are responsible for your server&lt;/strong&gt; — if your VPS goes down, your apps go down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some configuration is still required&lt;/strong&gt; — especially for custom setups, firewalls, and advanced networking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is self-hosted&lt;/strong&gt; — meaning you need to keep Coolify itself updated and maintained.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not ideal for very large scale&lt;/strong&gt; — for enterprise applications with massive traffic you may need dedicated infrastructure beyond a single VPS.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How Do You Get Started?
&lt;/h2&gt;

&lt;p&gt;Getting Coolify up and running is surprisingly straightforward. In an upcoming video I will walk you through the complete setup — from provisioning a VPS to having Coolify installed and your first application deployed.&lt;/p&gt;

&lt;p&gt;All you need to get started is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A VPS with at least &lt;strong&gt;2GB RAM&lt;/strong&gt; and &lt;strong&gt;2 CPU cores&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A domain name&lt;/li&gt;
&lt;li&gt;About &lt;strong&gt;30 minutes&lt;/strong&gt; of your time&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Coolify bridges the gap between the simplicity of managed platforms and the power and economics of a VPS. For developers and small teams who want to own their infrastructure without being overwhelmed by server management, it is genuinely one of the best tools available right now.&lt;/p&gt;

&lt;p&gt;In an upcoming video we will get our hands dirty and set up Coolify from scratch. See you there.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://coolify.io" rel="noopener noreferrer"&gt;Coolify Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://coolify.io/docs" rel="noopener noreferrer"&gt;Coolify Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/coollabsio/coolify" rel="noopener noreferrer"&gt;Coolify GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;🔔 Subscribe to my YouTube channel for the full series on building a modern web app back end from scratch.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>coolify</category>
      <category>vps</category>
      <category>docker</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Back-End Web Development — VPS vs Vercel vs Netlify</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Tue, 12 May 2026 01:14:25 +0000</pubDate>
      <link>https://dev.to/wadethomastt/back-end-web-development-vps-vs-vercel-vs-netlify-3cok</link>
      <guid>https://dev.to/wadethomastt/back-end-web-development-vps-vs-vercel-vs-netlify-3cok</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;🎬 This article is a companion to my YouTube video. Watch it here:&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/jxMMyRrWcyk"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Every great web application with millions of users has an even greater back end — and it has to be. Accessing your app over a prolonged period of time will test the integrity, availability, and speed of your back end. Backend technology can be inexpensive, or it can cost you thousands of dollars. So what's the deal here?&lt;/p&gt;

&lt;p&gt;Let me warn you that this channel is highly opinionated. I am sharing the tools that I use and how I use them.&lt;/p&gt;

&lt;p&gt;For my back end that powers my web apps, I use &lt;strong&gt;Directus&lt;/strong&gt; for my headless CMS, supported by a &lt;strong&gt;PostgreSQL&lt;/strong&gt; and &lt;strong&gt;Redis&lt;/strong&gt; database, hosted on a &lt;strong&gt;VPS&lt;/strong&gt;, and managed by &lt;strong&gt;Coolify&lt;/strong&gt;. Wow, that is a mouthful — and in this series we will explore how they all work together.&lt;/p&gt;

&lt;p&gt;So let's tackle each technology one at a time, starting with the VPS.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is a VPS?
&lt;/h2&gt;

&lt;p&gt;A Virtual Private Server, known as a VPS, is a virtual environment created on a physical server. Think of it like an apartment building — everyone shares the same physical structure, but each unit has its own private space, utilities, and security.&lt;/p&gt;

&lt;p&gt;While multiple VPS instances share a physical server, be careful not to confuse this with shared hosting. Each VPS is allocated its own dedicated resources, which are restricted to that VPS for as long as it is active. Therefore, the performance of a VPS is not directly affected by the usage of other VPS instances, but rather by the underlying performance of the physical server itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  What are the Benefits of a VPS?
&lt;/h2&gt;

&lt;p&gt;There are several benefits to consider when deciding on virtual private server hosting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Greater control&lt;/strong&gt; — Compared to shared hosting, you have root access and can fully customize your server environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated resources&lt;/strong&gt; — Allocated CPU, memory, and storage help ensure consistent performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt; — You can easily scale resources up or down to accommodate changing traffic and application demands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost-effectiveness&lt;/strong&gt; — A VPS typically offers a balance between the affordability of shared hosting and the power of a dedicated server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved security&lt;/strong&gt; — Isolation from other users on the same physical server enhances security.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choice of operating system&lt;/strong&gt; — You can choose the operating system that best suits your needs, such as Linux or Windows.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How Does it Stack Up Against Vercel or Netlify?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Vercel
&lt;/h3&gt;

&lt;p&gt;Vercel offers minimal setup with Git integration and fast deployment right out of the box. It has a free Hobby plan, but it is strictly for personal, non-commercial use. Once your app grows and needs to scale, you will need to move to a paid plan.&lt;/p&gt;

&lt;p&gt;The Pro plan starts at &lt;strong&gt;$20 per user per month&lt;/strong&gt;, and also covers serverless functions. Despite the name, serverless code still runs on a physical server — however, the infrastructure management, such as scaling, security patching, and provisioning, is handled entirely by the cloud provider.&lt;/p&gt;

&lt;p&gt;The main factor to watch carefully is cost. Vercel uses usage-based billing, and costs can escalate quickly — especially since Turbo build machines became the default for new Pro projects in February 2026, at &lt;strong&gt;$0.126 per build minute&lt;/strong&gt;. A moderate team workflow can generate over &lt;strong&gt;$400 per month&lt;/strong&gt; in build costs alone, before bandwidth and compute charges. I have seen some alarming Vercel bills shared online by customers, which means you need to fully understand the pricing model before you commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Netlify
&lt;/h3&gt;

&lt;p&gt;Netlify has historically been more suited to static sites and composable web applications. Composable applications are software systems built from independent, interchangeable modules rather than a single rigid codebase. Netlify also has Git integration, customizable build plugins, and serverless functions.&lt;/p&gt;

&lt;p&gt;Netlify moved to a credit-based pricing model in September 2025, designed to simplify metered billing. As of April 2026, the Pro plan costs &lt;strong&gt;$20 per month&lt;/strong&gt; and now includes unlimited team member seats, which is an improvement over the previous per-seat model. However, teams working on active projects with real traffic can burn through their credit allocation quickly, and credit pack add-ons at &lt;strong&gt;$10 per 1,500 credits&lt;/strong&gt; mean teams can regularly spend $40 to $80 or more per month beyond the base subscription. Your overall control is also more limited compared to a VPS, and you will still face a monthly cost as your app grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  The VPS
&lt;/h3&gt;

&lt;p&gt;VPS prices can grow as your app grows, but in my experience at a slower and more predictable rate — with no surprise charges. However, if you manage a VPS yourself, it requires understanding web servers, firewalls, operating systems, caching, and more. It can get complex very quickly and may cause you to spend more time managing the server than building and scaling your app.&lt;/p&gt;

&lt;p&gt;That is why some developers and business owners hire a team to handle it. Small teams may not have the budget for a dedicated infrastructure team, so they often opt for a more convenient managed solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Coolify — The Game Changer
&lt;/h2&gt;

&lt;p&gt;There is one tool that I think is worth mentioning and is a genuine game changer — and that is &lt;strong&gt;Coolify&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Coolify is a self-hostable platform that makes managing your VPS significantly easier. It offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Git integration&lt;/li&gt;
&lt;li&gt;✅ Dockerized container deployments&lt;/li&gt;
&lt;li&gt;✅ Automatic HTTPS&lt;/li&gt;
&lt;li&gt;✅ Built-in web server with no manual configuration required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the VPS a very real and competitive option worth considering. If you choose a solid hosting provider and pair it with the right tools, you can make this an absolute dream to work with.&lt;/p&gt;

&lt;p&gt;Now don't get me wrong — even with Coolify there are some configurations you will have to make. But I do believe a VPS is a great solution for a startup, mid-size, or enterprise scaled app with the right tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Stay with me in this series and we will definitely explore the possibilities. See you in the next video.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/learn/what-is-a-virtual-private-server" rel="noopener noreferrer"&gt;Google Cloud — What is a VPS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/what-is/vps/" rel="noopener noreferrer"&gt;AWS — What is a VPS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ibm.com/think/topics/vps" rel="noopener noreferrer"&gt;IBM — VPS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.scalahosting.com/blog/what-is-a-vps-technical-explanation/" rel="noopener noreferrer"&gt;Scala Hosting — VPS Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dreamhost.com/blog/beginners-guide-vps/" rel="noopener noreferrer"&gt;DreamHost — Beginner's Guide to VPS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/pricing" rel="noopener noreferrer"&gt;Vercel Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.netlify.com/pricing/" rel="noopener noreferrer"&gt;Netlify Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bejamas.com/blog/self-hosting-vs-vercel-and-netlify-which-solution-is-right" rel="noopener noreferrer"&gt;Bejamas — Self Hosting vs Vercel and Netlify&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;🔔 Subscribe to my YouTube channel for the full series on building a modern web app back end from scratch.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>coolify</category>
      <category>netlify</category>
      <category>vercel</category>
    </item>
    <item>
      <title>Protecting Routes in TanStack Start with Zustand</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Mon, 30 Mar 2026 04:54:07 +0000</pubDate>
      <link>https://dev.to/wadethomastt/protecting-routes-in-tanstack-start-with-zustand-2ci9</link>
      <guid>https://dev.to/wadethomastt/protecting-routes-in-tanstack-start-with-zustand-2ci9</guid>
      <description>&lt;p&gt;Route protection in TanStack Start doesn't have a built-in auth guard out of the box, but combining a Zustand store with a layout route gives you a clean, reusable solution that works well with SSR and hydration.&lt;/p&gt;

&lt;p&gt;Here's the full setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Auth Store
&lt;/h2&gt;

&lt;p&gt;First, the Zustand store. It persists the user to localStorage via the &lt;code&gt;persist&lt;/code&gt; middleware and uses a &lt;code&gt;hasHydrated&lt;/code&gt; flag to track when the store has been rehydrated from storage. This is critical — without it you'll get a flash of redirect on page load even for logged-in users.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;persist&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zustand/middleware&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;logoutUser&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/directus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AuthState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;hasHydrated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setHasHydrated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isLoggedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useAuthStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AuthState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;
  &lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;hasHydrated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;setHasHydrated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;hasHydrated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;logoutUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;isLoggedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;onRehydrateStorage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;setHasHydrated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;onRehydrateStorage&lt;/code&gt; callback fires once Zustand has finished reading from localStorage and sets &lt;code&gt;hasHydrated&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;. This is the signal your protected layout waits for before making any redirect decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The User Type
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;User&lt;/code&gt; type maps directly to the Directus user fields you need in your app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;location&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Protected Layout Route
&lt;/h2&gt;

&lt;p&gt;TanStack Start's file-based routing lets you create layout routes that wrap child routes. The &lt;code&gt;_protectedLayout&lt;/code&gt; route acts as an auth guard — it checks the store and redirects to login if no user is present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MainLayout&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/layouts/MainLayout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useNavigate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useAuthStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/store/authStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/_protectedLayout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProtectedLayout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;notFoundComponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;This page doesn't exist!&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProtectedLayout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hasHydrated&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuthStore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;navigate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNavigate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasHydrated&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hasHydrated&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasHydrated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MainLayout&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two guard conditions are important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;if (!hasHydrated) return null&lt;/code&gt; — waits for the store to rehydrate before rendering anything, preventing a flash of redirect&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;if (!user) return null&lt;/code&gt; — prevents rendering the layout while the redirect is in flight&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only when both conditions pass does &lt;code&gt;MainLayout&lt;/code&gt; render.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Main Layout
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MainLayout&lt;/code&gt; is a standard shell component that renders your header, footer and the current route's content via &lt;code&gt;Outlet&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Outlet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Header&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../Header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Footer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../Footer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MainLayout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"max-w-7xl mx-auto p-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Outlet&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Footer&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any route nested under &lt;code&gt;_protectedLayout&lt;/code&gt; automatically gets the auth guard and the shared layout — no need to add protection logic to individual routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It All Fits Together
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;User logs in → &lt;code&gt;setUser&lt;/code&gt; is called → Zustand persists the user to localStorage&lt;/li&gt;
&lt;li&gt;User revisits the app → Zustand rehydrates from localStorage → &lt;code&gt;hasHydrated&lt;/code&gt; is set to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Protected layout checks &lt;code&gt;hasHydrated&lt;/code&gt; and &lt;code&gt;user&lt;/code&gt; → renders &lt;code&gt;MainLayout&lt;/code&gt; if both pass&lt;/li&gt;
&lt;li&gt;User logs out → &lt;code&gt;logout&lt;/code&gt; calls Directus, clears the user from the store → redirect to login&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;This pattern keeps auth logic in one place, works cleanly with SSR hydration, and scales to as many protected routes as you need without repeating any guard logic.&lt;/p&gt;

&lt;p&gt;Have questions about the setup? Drop a comment below.&lt;/p&gt;

</description>
      <category>tanstack</category>
      <category>zustand</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Directus Auth out of the Box — Registration, Login, Email Verification and Password Reset</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Mon, 30 Mar 2026 04:37:13 +0000</pubDate>
      <link>https://dev.to/wadethomastt/directus-auth-out-of-the-box-registration-login-email-verification-and-password-reset-5dba</link>
      <guid>https://dev.to/wadethomastt/directus-auth-out-of-the-box-registration-login-email-verification-and-password-reset-5dba</guid>
      <description>&lt;p&gt;One of the things I appreciate most about Directus is that authentication is built in. Registration, login, email verification, and password reset — all handled without reaching for a third party auth service like Auth0 or Clerk.&lt;/p&gt;

&lt;p&gt;Here's how I implement the full auth flow in a TanStack Start app using the Directus SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  User Registration
&lt;/h2&gt;

&lt;p&gt;The SDK's &lt;code&gt;registerUser&lt;/code&gt; function handles registration. You pass the email, password and any additional fields. The &lt;code&gt;verification_url&lt;/code&gt; tells Directus where to redirect the user after they click the link in their email — Directus appends the token to that URL automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;verification_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_TANSTACK_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/auth/verify-email`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;directus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;registerUserDirectus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Directus sends the verification email automatically via your configured SMTP provider. I use Resend — SMTP is configured directly in the Docker Compose file via environment variables and the whole setup takes just a few minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;EMAIL_TRANSPORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smtp&lt;/span&gt;
&lt;span class="na"&gt;EMAIL_SMTP_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smtp.resend.com&lt;/span&gt;
&lt;span class="na"&gt;EMAIL_SMTP_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;465&lt;/span&gt;
&lt;span class="na"&gt;EMAIL_SMTP_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;resend&lt;/span&gt;
&lt;span class="na"&gt;EMAIL_SMTP_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your_resend_api_key&lt;/span&gt;
&lt;span class="na"&gt;EMAIL_FROM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;noreply@yourdomain.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Login
&lt;/h2&gt;

&lt;p&gt;Logging in is a single call on the Directus client. Session cookies are handled automatically thanks to the &lt;code&gt;credentials: 'include'&lt;/code&gt; config set up on the client.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loginUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;directus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, no token management, no localStorage juggling. The session is maintained via cookie.&lt;/p&gt;

&lt;h2&gt;
  
  
  Email Verification
&lt;/h2&gt;

&lt;p&gt;When the user clicks the link in their verification email, they land on your &lt;code&gt;/auth/verify-email&lt;/code&gt; route with a &lt;code&gt;token&lt;/code&gt; in the query string. You take that token and hit Directus's verify endpoint directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;VerifyEmailComponent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useSearch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verifying&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verifying&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_DIRECTUS_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/users/register/verify-email?token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;status&lt;/code&gt; state drives your UI — show a spinner while verifying, a success message on completion, or an error state if the token is invalid or expired.&lt;/p&gt;

&lt;h2&gt;
  
  
  Password Reset
&lt;/h2&gt;

&lt;p&gt;The password reset flow works the same way — Directus emails the user a reset link with a token, they land on your reset page, and you POST the new password along with the token to Directus's reset endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FormEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Passwords do not match.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_DIRECTUS_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/auth/password/reset`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to reset password. The link may have expired.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Something went wrong. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 204 response means success — redirect the user to login. Handle expired tokens gracefully with a clear error message.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Directus Handles For You
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Sending the verification email — via your SMTP provider configured in Docker Compose&lt;/li&gt;
&lt;li&gt;✅ Generating and validating tokens&lt;/li&gt;
&lt;li&gt;✅ Sending the password reset email&lt;/li&gt;
&lt;li&gt;✅ Session management via cookies&lt;/li&gt;
&lt;li&gt;✅ Secure password storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All you need is your SMTP credentials added to your Docker Compose file. I use Resend for email delivery — it's straightforward to set up and works reliably. Everything else is handled by Directus out of the box.&lt;/p&gt;




&lt;p&gt;This is the auth setup I use across all my TanStack Start projects. Combined with Directus roles and permissions, it covers everything a production app needs without adding a separate auth service to your stack.&lt;/p&gt;

&lt;p&gt;Have questions about the setup? Drop a comment below.&lt;/p&gt;

</description>
      <category>directus</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>Why I Switched to a VPS with Coolify for Hosting My Full Stack Apps</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Mon, 30 Mar 2026 04:22:24 +0000</pubDate>
      <link>https://dev.to/wadethomastt/why-i-switched-to-a-vps-with-coolify-for-hosting-my-full-stack-apps-3hce</link>
      <guid>https://dev.to/wadethomastt/why-i-switched-to-a-vps-with-coolify-for-hosting-my-full-stack-apps-3hce</guid>
      <description>&lt;p&gt;There are plenty of great hosting options out there — Netlify, Vercel, Railway and more. I've tried a few of them and they all have their place. But after going through a few different setups, I landed on a VPS managed with Coolify and it's been the best experience for my workflow.&lt;/p&gt;

&lt;p&gt;Here's how I got there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I Started
&lt;/h2&gt;

&lt;p&gt;I started with Netlify. It's beginner friendly, deployment is simple and it works well for frontend projects. I then tried Vercel and that was also a solid experience — great DX, fast deploys, good defaults.&lt;/p&gt;

&lt;p&gt;Both are genuinely good platforms. But as my projects grew more complex — running a frontend, a Directus instance, a database — the platform costs and constraints started to feel limiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Raw VPS Experiment
&lt;/h2&gt;

&lt;p&gt;I decided to try self-hosting on an Ubuntu VPS with Nginx as my web server and reverse proxy. Manually configuring everything — SSL, domains, process management, deployments. It was a solid learning experience and I'd recommend it to anyone who wants to understand how hosting actually works under the hood.&lt;/p&gt;

&lt;p&gt;But it wasn't the ideal long-term setup. Too much manual work, too much that could go wrong quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovering Coolify
&lt;/h2&gt;

&lt;p&gt;Coolify changed things. It's a self-hosted platform that sits on top of your VPS and gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic deployments&lt;/strong&gt; — push code and your frontend deploys automatically via Git integration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose support&lt;/strong&gt; — I manage my Directus instance and PostgreSQL database through Coolify using Docker Compose, which keeps everything clean and reproducible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse proxy handled for you&lt;/strong&gt; — no more manually writing Nginx configs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSL out of the box&lt;/strong&gt; — certificates are handled automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No hidden fees&lt;/strong&gt; — you pay for the VPS, that's it. Coolify itself is free to self-host&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Full Control, Your Way
&lt;/h2&gt;

&lt;p&gt;With a VPS you decide how it's configured. You're not subject to platform pricing changes, compute limits, or vendor lock-in. Everything runs on infrastructure you control.&lt;/p&gt;

&lt;p&gt;The tradeoff is responsibility — you manage the server, handle updates, and own any issues that come up. It's not the right fit for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is It Worth It?
&lt;/h2&gt;

&lt;p&gt;If you're comfortable with a bit of server management and want a flexible, cost-effective setup for running full stack apps, a VPS with Coolify is hard to beat. It's the sweet spot between the ease of a managed platform and the control of raw infrastructure.&lt;/p&gt;

&lt;p&gt;For my stack — TanStack Start on the frontend, Directus as the backend, PostgreSQL as the database — it handles everything cleanly in one place.&lt;/p&gt;




&lt;p&gt;Have questions about the setup or want to know if it's a good fit for your project? Drop a comment below.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>webdev</category>
      <category>selfhosted</category>
      <category>coolify</category>
    </item>
    <item>
      <title>Why I Use Directus as My Backend — Flexible, Self-Hosted and Production Ready</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Mon, 30 Mar 2026 04:15:39 +0000</pubDate>
      <link>https://dev.to/wadethomastt/why-i-use-directus-as-my-backend-flexible-free-and-production-ready-g6g</link>
      <guid>https://dev.to/wadethomastt/why-i-use-directus-as-my-backend-flexible-free-and-production-ready-g6g</guid>
      <description>&lt;p&gt;If you're looking for a backend that's flexible, affordable and genuinely enjoyable to work with, Directus is worth a serious look.&lt;/p&gt;

&lt;p&gt;Here's why it's become my default choice for full stack projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free to Self-Host (With a Sensible Threshold)
&lt;/h2&gt;

&lt;p&gt;Directus uses a BSL 1.1 license. If your organization has less than $5M in total annual income — revenue and funding combined — you can self-host it for free across all your projects, including production and commercial ones. No feature paywalls, no artificial limits.&lt;/p&gt;

&lt;p&gt;Once you're past that threshold, a commercial license is required. But for the vast majority of indie developers, freelancers, startups and small teams, it's completely free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Works with Any SQL Database
&lt;/h2&gt;

&lt;p&gt;Directus sits on top of your existing database rather than replacing it. It supports PostgreSQL, MySQL, SQLite, MariaDB and more. My preference is PostgreSQL — pair them together and you get a rock solid data layer with Directus handling the API, auth, and admin UI on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flexible Data Structures
&lt;/h2&gt;

&lt;p&gt;Whether you're building a simple blog, an e-commerce store, or a complex multi-tenant app, Directus adapts to your data model. You define your collections and fields, and Directus generates a full REST and GraphQL API automatically. No rigid schema to fight against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built-In Features That Scale With You
&lt;/h2&gt;

&lt;p&gt;Starting small? Directus is simple enough to get up and running fast. Ready to grow? It has you covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; — built in, with session and token support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roles and Permissions&lt;/strong&gt; — granular access control per collection and field&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation&lt;/strong&gt; — flows and webhooks for backend logic without extra services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard&lt;/strong&gt; — a full admin UI out of the box for managing your data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Management&lt;/strong&gt; — upload, transform and serve assets directly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Actively Maintained
&lt;/h2&gt;

&lt;p&gt;Directus is backed by a strong team of developers and an active open source community. It's regularly updated, well documented, and not going anywhere. That matters when you're building something you plan to maintain long term.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who It's For
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Startups&lt;/strong&gt; — get a full backend running quickly without over-engineering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freelancers&lt;/strong&gt; — one backend you can reuse across different client projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growing products&lt;/strong&gt; — the feature set scales as your requirements do&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developers who want control&lt;/strong&gt; — self-host it, own your data, no vendor lock-in&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I use Directus with TanStack Start on the frontend and the Directus SDK for all data fetching. If you're exploring it for your next project and want to see how the integration works in practice, I've covered the SDK setup and data fetching patterns in previous posts on my profile.&lt;/p&gt;

&lt;p&gt;Have a project in mind and want to know if Directus is the right fit? Drop a comment and I'm happy to help.&lt;/p&gt;

</description>
      <category>direct</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>backend</category>
    </item>
    <item>
      <title>TanStack Start: Metadata, Data Loading and Loading Skeletons in One Route File</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Mon, 30 Mar 2026 04:11:22 +0000</pubDate>
      <link>https://dev.to/wadethomastt/tanstack-start-metadata-data-loading-and-loading-skeletons-in-one-route-file-1cpi</link>
      <guid>https://dev.to/wadethomastt/tanstack-start-metadata-data-loading-and-loading-skeletons-in-one-route-file-1cpi</guid>
      <description>&lt;p&gt;TanStack Start has become my go-to for full stack React apps. One of the things I enjoy most about it is how cleanly it handles three things that are usually spread across multiple files and libraries — metadata, data fetching, and loading UI — all colocated in a single route.&lt;/p&gt;

&lt;p&gt;Here's how I use &lt;code&gt;head&lt;/code&gt;, &lt;code&gt;loader&lt;/code&gt;, and &lt;code&gt;pendingComponent&lt;/code&gt; together.&lt;/p&gt;

&lt;h2&gt;
  
  
  head — Per-Route Metadata
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;head&lt;/code&gt; lets you define document-level metadata per route. Page title, meta description, open graph tags — all scoped to that route without a global workaround or a third party head manager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;All Articles — Big Cats&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Every story, every species.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each route owns its own SEO info. No context providers, no helmet libraries, no prop drilling.&lt;/p&gt;

&lt;h2&gt;
  
  
  loader — Data Fetching Before Render
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;loader&lt;/code&gt; is where you fetch everything your route needs. It runs on the server or client depending on your SSR config, and the data it returns is available in your component via &lt;code&gt;useLoaderData&lt;/code&gt;. Parallel fetches with &lt;code&gt;Promise.all&lt;/code&gt; are the standard pattern here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;getCategories&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;categories&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both fetches run in parallel — no waterfall, no useEffect, no loading state management in the component. The data is just there when the component renders.&lt;/p&gt;

&lt;h2&gt;
  
  
  pendingComponent — Skeleton While the Loader Resolves
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pendingComponent&lt;/code&gt; renders while the loader is still resolving. It's the right place for skeletons or placeholder UI. The &lt;code&gt;pendingMs&lt;/code&gt; and &lt;code&gt;pendingMinMs&lt;/code&gt; options give you timing control — useful for avoiding a skeleton flash on fast connections while still showing it on slow ones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;pendingComponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"min-h-screen bg-black text-white"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"border-b border-white/10 px-6 py-16 text-center"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-4xl font-bold tracking-tight md:text-6xl"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        All Articles
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-3 text-white/50"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Every story, every species.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mx-auto max-w-6xl px-6 py-16"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PostGridSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The layout and headings render immediately while &lt;code&gt;PostGridSkeleton&lt;/code&gt; holds the place for the actual content. The page feels instant even on slower connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  All Together in One Route
&lt;/h2&gt;

&lt;p&gt;What I appreciate most is that all three live in the same route file. The metadata, the data fetching, and the loading UI are colocated — no jumping between files to understand what a route does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;All Articles — Big Cats&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Every story, every species.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nf"&gt;getCategories&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;categories&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;pendingComponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"min-h-screen bg-black text-white"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"border-b border-white/10 px-6 py-16 text-center"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-4xl font-bold tracking-tight md:text-6xl"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          All Articles
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-3 text-white/50"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Every story, every species.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mx-auto max-w-6xl px-6 py-16"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PostGridSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're evaluating TanStack Start for your next full stack project, this colocation pattern alone is worth the look. Everything your route needs — SEO, data, loading UI — in one place.&lt;/p&gt;




&lt;p&gt;I pair this with Directus as my CMS and Tailwind CSS v4 for styling. The &lt;code&gt;getPosts()&lt;/code&gt; and &lt;code&gt;getCategories()&lt;/code&gt; calls come from a single &lt;code&gt;directus.ts&lt;/code&gt; file using the Directus SDK — I covered that setup in a previous post if you want to see how that layer is structured.&lt;/p&gt;

</description>
      <category>tanstack</category>
      <category>react</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
