<?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: DevForge Templates</title>
    <description>The latest articles on DEV Community by DevForge Templates (@devforgedev).</description>
    <link>https://dev.to/devforgedev</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3834876%2Fbbe0aa1e-f149-43eb-8c6b-42d0206e4be2.png</url>
      <title>DEV Community: DevForge Templates</title>
      <link>https://dev.to/devforgedev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devforgedev"/>
    <language>en</language>
    <item>
      <title>Why Claude Code Changed How I Freelance as a Developer</title>
      <dc:creator>DevForge Templates</dc:creator>
      <pubDate>Tue, 14 Apr 2026 20:54:27 +0000</pubDate>
      <link>https://dev.to/devforgedev/why-claude-code-changed-how-i-freelance-as-a-developer-m2g</link>
      <guid>https://dev.to/devforgedev/why-claude-code-changed-how-i-freelance-as-a-developer-m2g</guid>
      <description>&lt;p&gt;I've been freelancing for years — React dashboards, Node.js APIs, the usual. In early 2026, I started using Claude Code as my primary development tool. Three months later, my workflow is unrecognizable.&lt;/p&gt;

&lt;p&gt;Here's what changed and why it matters for freelance developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Speed Difference
&lt;/h2&gt;

&lt;p&gt;A typical client project — say, a dashboard with auth, CRUD, charts, and deployment — used to take me 2-3 weeks. With Claude Code, I deliver in 3-5 days.&lt;/p&gt;

&lt;p&gt;Not because I'm cutting corners. The code quality is higher. Tests are written. Docker deployment is included. The AI handles the boilerplate while I focus on architecture decisions and client-specific logic.&lt;/p&gt;

&lt;p&gt;For freelancers, speed = more projects = more revenue. At $45-80/hr, cutting project time by 60% means you can take on 2-3x the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Use It For
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Multi-Agent Coordination
&lt;/h3&gt;

&lt;p&gt;I run 7 Claude Code agents across 16 projects. A Boss agent coordinates, worker agents handle specific domains (trading, marketing, research). They communicate via a file-based message bus — no frameworks, no dependencies.&lt;/p&gt;

&lt;p&gt;This isn't a demo. The trading agent manages real money on Binance. The marketing agent publishes articles autonomously. The research agent crawls the web for opportunities.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Rapid Prototyping for Clients
&lt;/h3&gt;

&lt;p&gt;When a client says "I need an investor dashboard," I don't start from scratch. I tell Claude Code about the requirements and it scaffolds the project in minutes, using patterns it's seen across my 16 projects. Custom Prisma schema, API routes, React components — all type-safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Code Review and Refactoring
&lt;/h3&gt;

&lt;p&gt;Claude Code's hook system lets me enforce rules mechanically. Before any file edit, a hook checks for security issues, validates against my coding standards, and blocks writes to protected files. It's like having a senior reviewer that never sleeps.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack That Works
&lt;/h2&gt;

&lt;p&gt;After 12+ production projects, I've settled on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React 19 + TypeScript&lt;/strong&gt; — frontend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fastify 5 + Prisma 7&lt;/strong&gt; — backend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; — database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; — deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zod&lt;/strong&gt; — validation across the stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mantine 8&lt;/strong&gt; — UI components&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every project uses the same stack. Claude Code knows it deeply. New projects start at 80% done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advice for Freelancers Considering Claude Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with CLAUDE.md.&lt;/strong&gt; This file tells Claude Code about your project — commands, architecture, rules. Keep it under 60 lines. More than that, and the AI starts ignoring instructions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use hooks, not instructions.&lt;/strong&gt; If something must happen every time (check inbox, validate files, update state), put it in a hook. Hooks are mechanical. Instructions are suggestions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specialize.&lt;/strong&gt; "React developer who uses AI" is commodity. "Claude Code expert who builds multi-agent systems" is a niche. The AI freelance market grew 178% in 2025. Position accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async everything.&lt;/strong&gt; Claude Code works best when you give it a task and let it run. I work async with all clients — no calls, no screen shares. I deliver working code, not explanations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Market
&lt;/h2&gt;

&lt;p&gt;AI-assisted development isn't a gimmick. Upwork's AI category grew 60% year-over-year. Claude Code specialists command 40-60% rate premiums over general developers.&lt;/p&gt;

&lt;p&gt;The freelancers who will thrive in 2026 aren't the ones who resist AI — they're the ones who make it their competitive advantage.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build AI-powered fullstack applications with Claude Code. Available for async freelance work — dashboards, AI integrations, multi-agent systems. Portfolio: &lt;a href="https://devforgetemplates.gumroad.com" rel="noopener noreferrer"&gt;devforgetemplates.gumroad.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>freelancing</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How I Built a Multi-Agent System Using Only Claude Code (No Frameworks)</title>
      <dc:creator>DevForge Templates</dc:creator>
      <pubDate>Tue, 14 Apr 2026 20:52:47 +0000</pubDate>
      <link>https://dev.to/devforgedev/how-i-built-a-multi-agent-system-using-only-claude-code-no-frameworks-5bo1</link>
      <guid>https://dev.to/devforgedev/how-i-built-a-multi-agent-system-using-only-claude-code-no-frameworks-5bo1</guid>
      <description>&lt;p&gt;I run 7 Claude Code agents that coordinate across 16 projects on a single machine. No CrewAI. No AutoGen. No LangGraph. Just files, shell scripts, and Claude Code's built-in hook system.&lt;/p&gt;

&lt;p&gt;This isn't a toy demo — it manages a crypto trading bot, marketing pipelines, a multiplayer game server, and home automation. Here's the architecture and how to build it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Use a Framework?
&lt;/h2&gt;

&lt;p&gt;Multi-agent frameworks operate at the API level — they orchestrate LLM calls through Python code. But Claude Code agents are terminal sessions with full filesystem access. They read files, run commands, edit code. The coordination layer needs to match that model.&lt;/p&gt;

&lt;p&gt;Files are the answer. Every agent can read and write files. No servers, no ports, no auth tokens, no dependencies to install.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Message Bus: 30 Lines of Bash
&lt;/h2&gt;

&lt;p&gt;The entire inter-agent communication layer is a directory with inbox files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.claude/bus/
├── inbox-boss.md        # Coordinator agent
├── inbox-trading.md     # Crypto trading bot
├── inbox-freelance.md   # Marketing &amp;amp; sales
├── inbox-research.md    # Web research specialist
├── send.sh              # Message delivery
├── broadcast.sh         # Send to all agents
├── audit.log            # Full message history
└── check-inbox.sh       # Read + archive + clear
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The send script validates known agents, blocks suspicious content (injection defense), and appends to the recipient's inbox:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# send.sh — FROM TO TYPE "message"&lt;/span&gt;
&lt;span class="nv"&gt;FROM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;TO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;3&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;FYI&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift &lt;/span&gt;3&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;KNOWN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"boss trading freelance research"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KNOWN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qw&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FROM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KNOWN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qw&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;span class="c"&gt;# Block command injection patterns&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qiE&lt;/span&gt; &lt;span class="s1"&gt;'rm -rf|drop table|curl.*\|.*sh'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TYPE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%H:%M&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;] from:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FROM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; | &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/bus/inbox-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TO&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="s1"&gt;'+%F %T'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; | &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FROM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TO&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; | &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TYPE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; | &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MSG&lt;/span&gt;:0:200&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/bus/audit.log"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real messages from my audit log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;freelance -&amp;gt; boss  | STATUS | Published 2 Dev.to articles, 144 views
research  -&amp;gt; boss  | DONE   | Task complete: implementation patterns saved
boss      -&amp;gt; trading | ACK  | Deploy #50 confirmed, monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hook-Driven Lifecycle
&lt;/h2&gt;

&lt;p&gt;Claude Code hooks fire at specific points in the session — mechanically, every time, regardless of context length. This is more reliable than instructions in CLAUDE.md (which degrade after ~150 lines of rules).&lt;/p&gt;

&lt;p&gt;Here's the settings.json that wires it together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SessionStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".claude/hooks/session-start.sh"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"UserPromptSubmit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.claude/bus/check-inbox.sh trading"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".claude/hooks/protect-files.sh"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreCompact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"echo 'SAVE STATE: Update SESSION-HANDOFF.md before compacting'"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each hook does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SessionStart&lt;/strong&gt; — loads shared rules, checks inbox, reads the session handoff document. The agent wakes up with full context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UserPromptSubmit&lt;/strong&gt; — checks the inbox before every response. Messages are picked up within one interaction, no polling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PreToolUse&lt;/strong&gt; — guards sensitive files. My hook blocks &lt;code&gt;.env&lt;/code&gt; edits and read-only research docs. The agent physically cannot write credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PreCompact&lt;/strong&gt; — forces the agent to save state before context compression. No more lost progress.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Shared Memory: Knowledge That Propagates
&lt;/h2&gt;

&lt;p&gt;When any agent discovers a bug or platform limitation, it writes to a shared error database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# shared/rules/errors.md&lt;/span&gt;

&lt;span class="gu"&gt;## API Limitations&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Gumroad: POST/PUT return 404 — create/edit via browser only
&lt;span class="p"&gt;-&lt;/span&gt; Dev.to: DELETE articles returns 404 HTML, not JSON

&lt;span class="gu"&gt;## Claude Code&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; CronCreate expires after 3 days — must recreate
&lt;span class="p"&gt;-&lt;/span&gt; Heredocs in background mode never terminate → zombie shells
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every agent reads this file at session start. When the trading agent discovers that asyncpg returns &lt;code&gt;Decimal&lt;/code&gt; types instead of &lt;code&gt;float&lt;/code&gt;, it logs the gotcha. Later, when another agent touches database code, it already knows. Knowledge propagates without human intervention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It: Minimal Two-Agent Setup
&lt;/h2&gt;

&lt;p&gt;Create this structure and run two Claude Code sessions:&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.claude/bus

&lt;span class="c"&gt;# Create inbox files&lt;/span&gt;
&lt;span class="nb"&gt;touch&lt;/span&gt; ~/.claude/bus/inbox-dev.md
&lt;span class="nb"&gt;touch&lt;/span&gt; ~/.claude/bus/inbox-reviewer.md

&lt;span class="c"&gt;# Create send script&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.claude/bus/send.sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;SCRIPT&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash
FROM="&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;"; TO="&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="sh"&gt;"; shift 2; MSG="&lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="sh"&gt;"
echo "[&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%H:%M&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;] from:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FROM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt; | &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;" &amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="sh"&gt;
  "&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="sh"&gt;/.claude/bus/inbox-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TO&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;.md"
&lt;/span&gt;&lt;span class="no"&gt;SCRIPT
&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ~/.claude/bus/send.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In project A's &lt;code&gt;.claude/settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"UserPromptSubmit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cat ~/.claude/bus/inbox-dev.md &amp;amp;&amp;amp; &amp;gt; ~/.claude/bus/inbox-dev.md"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the "reviewer" agent can send feedback that the "dev" agent sees on its next interaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/.claude/bus/send.sh reviewer dev &lt;span class="s2"&gt;"Auth middleware missing rate limiting — see src/middleware/auth.ts:47"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the foundation. The full system adds validation, audit logging, broadcast, and scoped rules — but this is enough to coordinate two agents on a real project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Works and What Doesn't
&lt;/h2&gt;

&lt;p&gt;After 3 months of running this in production:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Works well:&lt;/strong&gt; Low-frequency coordination (status updates, task delegation, error sharing), session continuity via handoff documents, scoped rules that load only when editing relevant files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doesn't work:&lt;/strong&gt; Real-time back-and-forth (file polling has latency), agents editing the same files simultaneously, complex multi-step workflows that need tight coupling (use a single agent instead).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Surprising win:&lt;/strong&gt; The shared error database. It turns individual debugging sessions into institutional knowledge. Every agent gets smarter from every other agent's mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Pattern
&lt;/h2&gt;

&lt;p&gt;This isn't really about multi-agent systems. It's about treating AI agents as first-class team members with proper tooling: inboxes, shared docs, lifecycle hooks, guarded resources, audit trails. The same patterns that make human teams effective — clear communication, shared context, defined responsibilities — work for AI agents too.&lt;/p&gt;

&lt;p&gt;The difference is you can set it up in an afternoon.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build systems like this for clients — AI integration, multi-agent coordination, and fullstack TypeScript projects. If you're exploring Claude Code for your team, my async consulting details are at &lt;a href="https://devforgetemplates.gumroad.com" rel="noopener noreferrer"&gt;DevForge Templates&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Minimal Docker Setup for Fullstack Development</title>
      <dc:creator>DevForge Templates</dc:creator>
      <pubDate>Fri, 03 Apr 2026 09:15:15 +0000</pubDate>
      <link>https://dev.to/devforgedev/the-minimal-docker-setup-for-fullstack-development-3e20</link>
      <guid>https://dev.to/devforgedev/the-minimal-docker-setup-for-fullstack-development-3e20</guid>
      <description>&lt;p&gt;You don't need a Dockerfile for local development. You need &lt;code&gt;docker-compose.yml&lt;/code&gt; with three services and a volume mount.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6379:6379"&lt;/span&gt;

  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:22-alpine&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules:/app/node_modules&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://dev:dev@db:5432/myapp&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://redis:6379&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sh -c "npm install &amp;amp;&amp;amp; npm run dev"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;node_modules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Run &lt;code&gt;docker compose up&lt;/code&gt; and you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL 17 with persistent data&lt;/li&gt;
&lt;li&gt;Redis for caching/sessions&lt;/li&gt;
&lt;li&gt;Your app with hot reload (Vite, nodemon, whatever your &lt;code&gt;dev&lt;/code&gt; script uses)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No Dockerfile for dev.&lt;/strong&gt; You don't need one. The &lt;code&gt;node:22-alpine&lt;/code&gt; image + volume mount gives you hot reload. Your code changes are reflected instantly because the current directory is mounted as &lt;code&gt;/app&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Named volume for node_modules.&lt;/strong&gt; This is the key trick. Without &lt;code&gt;node_modules:/app/node_modules&lt;/code&gt;, the volume mount from your host would overwrite the container's &lt;code&gt;node_modules&lt;/code&gt;. The named volume keeps them separate — container installs its own deps, your host keeps its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persistent database.&lt;/strong&gt; The &lt;code&gt;pgdata&lt;/code&gt; volume survives &lt;code&gt;docker compose down&lt;/code&gt;. Your seed data, migrations, test records — all preserved. Only &lt;code&gt;docker compose down -v&lt;/code&gt; wipes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Prisma Migrations
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;migrate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:22-alpine&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules:/app/node_modules&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://dev:dev@db:5432/myapp&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sh -c "npm install &amp;amp;&amp;amp; npx prisma migrate dev"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
    &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;migrate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run migrations: &lt;code&gt;docker compose --profile migrate run migrate&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;profiles&lt;/code&gt; key means this service only runs when explicitly called. It won't start with a regular &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Is Different
&lt;/h2&gt;

&lt;p&gt;For production, you DO need a Dockerfile. Multi-stage build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1: Build&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:22-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Stage 2: Run&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:22-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dev image: ~900MB (full Node + all deps + source). Production image: ~150MB (only build output + production deps).&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Forgetting the node_modules volume&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without it:&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;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app&lt;/span&gt;  &lt;span class="c1"&gt;# Host's empty node_modules overwrites container's&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With it:&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;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.:/app&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules:/app/node_modules&lt;/span&gt;  &lt;span class="c1"&gt;# Container keeps its own&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Using &lt;code&gt;build: .&lt;/code&gt; in development&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you use &lt;code&gt;build: .&lt;/code&gt; instead of &lt;code&gt;image: node:22-alpine&lt;/code&gt;, Docker rebuilds on every &lt;code&gt;docker compose up&lt;/code&gt;. Volume mount + pre-built image is faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Not setting &lt;code&gt;depends_on&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your app will crash on first start if it tries to connect to PostgreSQL before the database is ready. &lt;code&gt;depends_on&lt;/code&gt; ensures startup order (though for production, use healthchecks).&lt;/p&gt;

&lt;h2&gt;
  
  
  The .env.example
&lt;/h2&gt;

&lt;p&gt;Always ship one:&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;# .env.example&lt;/span&gt;
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql://dev:dev@localhost:5432/myapp
&lt;span class="nv"&gt;REDIS_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redis://localhost:6379
&lt;span class="nv"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev-secret-change-in-production
&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: &lt;code&gt;localhost&lt;/code&gt; for running outside Docker, &lt;code&gt;db&lt;/code&gt;/&lt;code&gt;redis&lt;/code&gt; for inside Docker. Use the Docker service names in &lt;code&gt;docker-compose.yml&lt;/code&gt; and &lt;code&gt;localhost&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; for when you run the app natively.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This setup runs across 12 production templates. Clone, &lt;code&gt;docker compose up&lt;/code&gt;, start coding. No 45-minute setup guides.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>webdev</category>
      <category>node</category>
      <category>postgres</category>
    </item>
    <item>
      <title>5 Claude Code Hooks That Automate My Entire Workflow</title>
      <dc:creator>DevForge Templates</dc:creator>
      <pubDate>Thu, 02 Apr 2026 06:21:45 +0000</pubDate>
      <link>https://dev.to/devforgedev/5-claude-code-hooks-that-automate-my-entire-workflow-2fj7</link>
      <guid>https://dev.to/devforgedev/5-claude-code-hooks-that-automate-my-entire-workflow-2fj7</guid>
      <description>&lt;p&gt;Claude Code hooks are shell commands that fire at specific lifecycle events. They're configured in &lt;code&gt;.claude/settings.json&lt;/code&gt; and run automatically — no manual intervention.&lt;/p&gt;

&lt;p&gt;I use 5 hooks across all my projects. Here's each one with the exact config.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Auto-Format After Every File Write
&lt;/h2&gt;

&lt;p&gt;Every time Claude writes or edits a file, Prettier runs on it automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write|Edit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; prettier --write &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; } 2&amp;gt;/dev/null || true"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Claude's code formatting is inconsistent. Sometimes it uses 2 spaces, sometimes 4. Prettier fixes it instantly. The &lt;code&gt;|| true&lt;/code&gt; ensures the hook never blocks Claude if Prettier fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Inbox Check on Every Message
&lt;/h2&gt;

&lt;p&gt;Every time I send a message, Claude checks its message bus inbox for tasks from other agents.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"UserPromptSubmit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.claude/bus/check-inbox.sh my-agent"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script checks if &lt;code&gt;~/.claude/bus/inbox-my-agent.md&lt;/code&gt; has content. If yes, it prints the messages. Claude sees them and acts on them before responding to my message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; This is how my 6 agents communicate. No polling interval — messages are delivered the moment I interact with any agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Session Context on Start
&lt;/h2&gt;

&lt;p&gt;When a new session starts, automatically load the last session's state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SessionStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cat docs/SESSION-HANDOFF.md 2&amp;gt;/dev/null &amp;amp;&amp;amp; echo '---' &amp;amp;&amp;amp; cat .claude/rules/known-issues.md 2&amp;gt;/dev/null || true"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Claude reads the handoff file and known issues automatically. No need to say "read the handoff file" at the start of every session. It just knows what happened last time.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Reminder Before Session Ends
&lt;/h2&gt;

&lt;p&gt;When Claude stops (session end, context limit, or user stops), remind it to save state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"echo '{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;systemMessage&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;REMINDER: Update docs/SESSION-HANDOFF.md with what was done, what failed, and what is next.&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}'"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Without this, Claude often stops mid-task without saving context. The reminder fires every time, so the handoff file stays current.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Protect Critical Files
&lt;/h2&gt;

&lt;p&gt;Before any edit to protected files, block the write.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jq -r '.tool_input.file_path // &lt;/span&gt;&lt;span class="se"&gt;\"\"&lt;/span&gt;&lt;span class="s2"&gt;' | grep -qE '(&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.env|credentials|secrets)' &amp;amp;&amp;amp; echo '{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;decision&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;reason&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Cannot edit sensitive files&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}' || true"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Claude occasionally tries to write API keys or modify &lt;code&gt;.env&lt;/code&gt; files. This hook blocks any edit to files matching sensitive patterns. It's a safety net you set once and forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hook Lifecycle
&lt;/h2&gt;

&lt;p&gt;Here's when each hook type fires:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SessionStart&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New session begins&lt;/td&gt;
&lt;td&gt;Load context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UserPromptSubmit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;You send a message&lt;/td&gt;
&lt;td&gt;Check inbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PreToolUse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Before a tool runs&lt;/td&gt;
&lt;td&gt;Block dangerous actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PostToolUse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;After a tool succeeds&lt;/td&gt;
&lt;td&gt;Format code, run tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Session ends&lt;/td&gt;
&lt;td&gt;Save state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PreCompact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Before context compression&lt;/td&gt;
&lt;td&gt;Preserve critical info&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Setting It Up
&lt;/h2&gt;

&lt;p&gt;All hooks go in &lt;code&gt;.claude/settings.json&lt;/code&gt; (project-level) or &lt;code&gt;~/.claude/settings.json&lt;/code&gt; (global).&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;# Create project settings&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; .claude
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .claude/settings.json &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "hooks": [{
        "type": "command",
        "command": "echo 'file written'"
      }]
    }]
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start simple. Add one hook. Verify it fires. Then add more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Missing &lt;code&gt;|| true&lt;/code&gt;&lt;/strong&gt; — If your hook command fails, it can block Claude entirely. Always add &lt;code&gt;|| true&lt;/code&gt; unless you intentionally want to block on failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Hooks that are too slow&lt;/strong&gt; — Hooks run synchronously by default. A hook that takes 10 seconds runs on every single tool call. Keep them fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Forgetting the JSON output format&lt;/strong&gt; — &lt;code&gt;Stop&lt;/code&gt; hooks need to output JSON with &lt;code&gt;systemMessage&lt;/code&gt; field. Plain text won't show up.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;These 5 hooks run across 16 projects. Once set up, they work silently in the background. The best automation is the kind you forget exists.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>automation</category>
      <category>claude</category>
    </item>
    <item>
      <title>The One File That Makes Claude Code Sessions Actually Persistent</title>
      <dc:creator>DevForge Templates</dc:creator>
      <pubDate>Wed, 01 Apr 2026 09:46:35 +0000</pubDate>
      <link>https://dev.to/devforgedev/the-one-file-that-makes-claude-code-sessions-actually-persistent-48n2</link>
      <guid>https://dev.to/devforgedev/the-one-file-that-makes-claude-code-sessions-actually-persistent-48n2</guid>
      <description>&lt;p&gt;Every Claude Code session ends the same way: context window fills up, session compresses, and your AI forgets what it was doing.&lt;/p&gt;

&lt;p&gt;I fixed this with one file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Claude Code has no persistent memory between sessions. When you start a new terminal, it reads your &lt;code&gt;CLAUDE.md&lt;/code&gt; and starts fresh. Any decisions made, approaches tried, or context built up — gone.&lt;/p&gt;

&lt;p&gt;For a single quick task, this is fine. For multi-day projects with complex state, it's a disaster.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: SESSION-HANDOFF.md
&lt;/h2&gt;

&lt;p&gt;Every project gets a &lt;code&gt;docs/SESSION-HANDOFF.md&lt;/code&gt; with this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Session Handoff&lt;/span&gt;

&lt;span class="gu"&gt;## Last Updated&lt;/span&gt;
2026-03-31 (Session 30)

&lt;span class="gu"&gt;## Current State&lt;/span&gt;
[What's working, what's deployed, what's in progress]

&lt;span class="gu"&gt;## What Was Done This Session&lt;/span&gt;
[Bullet points of completed work]

&lt;span class="gu"&gt;## What Failed&lt;/span&gt;
[Approaches that didn't work and why — so the next session doesn't repeat them]

&lt;span class="gu"&gt;## What's Next&lt;/span&gt;
[Prioritized list of next steps]

&lt;span class="gu"&gt;## Key Decisions&lt;/span&gt;
[Architectural choices made and the reasoning]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Rule
&lt;/h2&gt;

&lt;p&gt;The rule is simple and non-negotiable: &lt;strong&gt;CLAUDE.md mandates updating SESSION-HANDOFF.md before every session end or context compaction.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my &lt;code&gt;CLAUDE.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Session Protocol (MANDATORY)&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Start: MUST read docs/SESSION-HANDOFF.md
&lt;span class="p"&gt;2.&lt;/span&gt; During: note decisions in SESSION-HANDOFF.md
&lt;span class="p"&gt;3.&lt;/span&gt; Before end/compaction: MUST update SESSION-HANDOFF.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code follows this because it's in &lt;code&gt;CLAUDE.md&lt;/code&gt; — loaded on every session start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works Better Than Memory
&lt;/h2&gt;

&lt;p&gt;Claude Code has a built-in memory system (&lt;code&gt;~/.claude/&lt;/code&gt; memory files). It's useful for user preferences and project-level facts. But it's bad for session state because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Memory is unstructured.&lt;/strong&gt; SESSION-HANDOFF.md has a fixed format — Claude knows exactly where to look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory is append-only.&lt;/strong&gt; Session state changes completely each session. You need a full rewrite, not appended notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory survives too long.&lt;/strong&gt; A "what I was doing" note from 5 sessions ago is noise. SESSION-HANDOFF.md is always current.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Making It Bulletproof
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Hook: Reminder Before End
&lt;/h3&gt;

&lt;p&gt;Add a &lt;code&gt;Stop&lt;/code&gt; hook in &lt;code&gt;.claude/settings.json&lt;/code&gt; that reminds Claude to update the handoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"echo '{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;systemMessage&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;REMINDER: Update SESSION-HANDOFF.md before ending!&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}'"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hook: Auto-Read on Start
&lt;/h3&gt;

&lt;p&gt;Add a &lt;code&gt;SessionStart&lt;/code&gt; hook that reads the handoff file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SessionStart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cat docs/SESSION-HANDOFF.md 2&amp;gt;/dev/null || echo 'No handoff file found'"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Claude reads the last session's state automatically on every start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Example
&lt;/h2&gt;

&lt;p&gt;Here's a compressed version of my trading bot project's handoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Last Updated: Session 47&lt;/span&gt;

&lt;span class="gu"&gt;## Current State&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Bot running on VPS, 145 trades executed
&lt;span class="p"&gt;-&lt;/span&gt; Dashboard: 18 pages, all functional
&lt;span class="p"&gt;-&lt;/span&gt; OBV strategy v12 in production

&lt;span class="gu"&gt;## What Failed This Session&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Volume filter caused false negatives — rolled back in Deploy#47
&lt;span class="p"&gt;-&lt;/span&gt; html2canvas timeout on complex dashboard pages

&lt;span class="gu"&gt;## What's Next&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; [P0] Fix trailing stop calibration for volatile coins
&lt;span class="p"&gt;2.&lt;/span&gt; [P1] Add correlation matrix page to dashboard
&lt;span class="p"&gt;3.&lt;/span&gt; [P2] Backtest new momentum strategy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next session reads this and immediately knows: don't re-add the volume filter, focus on trailing stops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling to Multiple Agents
&lt;/h2&gt;

&lt;p&gt;When you run multiple Claude Code instances (I run 6), each has its own SESSION-HANDOFF.md. A Boss agent can read all of them:&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="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; ~/projects/&lt;span class="k"&gt;*&lt;/span&gt;/docs/SESSION-HANDOFF.md&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="si"&gt;))&lt;/span&gt;&lt;span class="s2"&gt; ==="&lt;/span&gt;
  &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the coordinator a snapshot of all projects in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern in 30 Seconds
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Add &lt;code&gt;docs/SESSION-HANDOFF.md&lt;/code&gt; to your project&lt;/li&gt;
&lt;li&gt;Add "MUST read/update SESSION-HANDOFF.md" to your &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add SessionStart hook to auto-read it&lt;/li&gt;
&lt;li&gt;Add Stop hook to remind about updates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total setup: 5 minutes. Saves hours of re-explaining context.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Running this pattern across 16 projects. Context persists. Sessions don't.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>devops</category>
      <category>claude</category>
    </item>
    <item>
      <title>How I Run a 3-Agent Content Marketing Team with Claude Code</title>
      <dc:creator>DevForge Templates</dc:creator>
      <pubDate>Mon, 30 Mar 2026 09:28:43 +0000</pubDate>
      <link>https://dev.to/devforgedev/how-i-run-a-3-agent-content-marketing-team-with-claude-code-49b8</link>
      <guid>https://dev.to/devforgedev/how-i-run-a-3-agent-content-marketing-team-with-claude-code-49b8</guid>
      <description>&lt;p&gt;I got tired of manually publishing to Dev.to, Hashnode, and Bluesky every time I wrote an article. So I built a 3-agent system that does it for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;Three Claude Code instances, each in its own terminal:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;What it owns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boss&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Strategist&lt;/td&gt;
&lt;td&gt;Content calendar, task delegation, analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Writer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Article creator&lt;/td&gt;
&lt;td&gt;Research, outline, draft, save to &lt;code&gt;drafts/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Social&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Publisher&lt;/td&gt;
&lt;td&gt;Dev.to API, Hashnode GraphQL, Bluesky AT Protocol&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They communicate through a file-based message bus. Boss sends a topic to Writer. Writer researches, drafts, and notifies Social. Social publishes everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Message Bus
&lt;/h2&gt;

&lt;p&gt;Four lines of bash:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;FROM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="nv"&gt;TO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt; &lt;span class="nv"&gt;TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift &lt;/span&gt;3&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="nv"&gt;$TYPE&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%H:%M&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;] from:&lt;/span&gt;&lt;span class="nv"&gt;$FROM&lt;/span&gt;&lt;span class="s2"&gt; | &lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.claude/bus/inbox-&lt;span class="nv"&gt;$TO&lt;/span&gt;.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent checks its inbox on every user message (via a &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook in &lt;code&gt;.claude/settings.json&lt;/code&gt;). When a message arrives, the agent reads it, executes, replies, and clears.&lt;/p&gt;

&lt;p&gt;No Redis. No RabbitMQ. No WebSockets. Just files.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a Publishing Cycle Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; I tell Boss what to write.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write an article about Zod validation patterns.
Target: intermediate React developers.
Angle: the patterns nobody talks about.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Boss delegates to Writer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/.claude/bus/send.sh boss writer TASK &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"topic: Zod validation, angle: hidden patterns, target: intermediate React devs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Writer researches (WebSearch), outlines, drafts 800-1200 words with working code examples, and saves to &lt;code&gt;drafts/2026-03-30-zod-patterns.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Writer notifies the team.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/.claude/bus/send.sh writer boss DONE &lt;span class="s2"&gt;"draft ready: drafts/2026-03-30-zod-patterns.md"&lt;/span&gt;
~/.claude/bus/send.sh writer social TASK &lt;span class="s2"&gt;"new draft ready for publish"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5:&lt;/strong&gt; Boss reviews, approves. Social publishes to all 3 platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Publishing Scripts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Dev.to&lt;/strong&gt; — one curl call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://dev.to/api/articles &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"api-key: &lt;/span&gt;&lt;span class="nv"&gt;$DEVTO_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @article.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Hashnode&lt;/strong&gt; — GraphQL mutation with canonical URL (no SEO penalty for cross-posting):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node tools/hashnode-publish.mjs drafts/article.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bluesky&lt;/strong&gt; — AT Protocol with auto-facets for link cards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node tools/bluesky-post.mjs &lt;span class="s2"&gt;"Hook text + article link"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three are autonomous. No browser needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results So Far
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;14 articles across Dev.to and Hashnode&lt;/li&gt;
&lt;li&gt;439 views in 10 days (from zero followers)&lt;/li&gt;
&lt;li&gt;Top article: 145 views (crypto trading bot)&lt;/li&gt;
&lt;li&gt;Publishing time per article: ~2 minutes across 3 platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with 2 agents, not 3.&lt;/strong&gt; Boss + Writer is enough initially. Add Social when you have enough drafts queued up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mandate session handoff.&lt;/strong&gt; Each agent must update &lt;code&gt;SESSION-HANDOFF.md&lt;/code&gt; before session end. Without this, you lose context on restart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't over-engineer the bus.&lt;/strong&gt; I tried adding message types (TASK, QUESTION, ACK, DONE) — most of the time TASK and DONE are all you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Key Insight
&lt;/h2&gt;

&lt;p&gt;The value isn't in the infrastructure. The bus is trivial. The publishing scripts took an afternoon.&lt;/p&gt;

&lt;p&gt;The value is in the CLAUDE.md files — the instructions that make each agent actually good at its job. Getting a Writer agent to produce articles that don't sound like AI slop takes weeks of tuning. Getting a Boss agent to delegate with the right context takes real iteration.&lt;/p&gt;

&lt;p&gt;You're buying architecture decisions, not code.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building AI agent systems in public. 14 articles, 6 agents, 0 frameworks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>writing</category>
      <category>claude</category>
    </item>
    <item>
      <title>Multi-Agent Claude System: How 6 AIs Work Together on My Projects</title>
      <dc:creator>DevForge Templates</dc:creator>
      <pubDate>Sun, 29 Mar 2026 06:26:20 +0000</pubDate>
      <link>https://dev.to/devforgedev/multi-agent-claude-system-how-6-ais-work-together-on-my-projects-2olb</link>
      <guid>https://dev.to/devforgedev/multi-agent-claude-system-how-6-ais-work-together-on-my-projects-2olb</guid>
      <description>&lt;p&gt;Tonight I held a conference call with 6 AIs. They each reported status, shared insights, and proposed ideas for each other's projects. This isn't science fiction — it's my actual development setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;I run 6 specialized Claude Code CLI instances, each owning a different project:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boss&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Coordinator&lt;/td&gt;
&lt;td&gt;Delegates tasks, reviews output, makes cross-project decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Binance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trading bot&lt;/td&gt;
&lt;td&gt;Crypto strategy development, backtesting, live trading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Freelance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Template factory&lt;/td&gt;
&lt;td&gt;Gumroad products, marketing, Upwork proposals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Home Assistant&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Smart home&lt;/td&gt;
&lt;td&gt;IoT automations, sensor dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Xojo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Desktop apps&lt;/td&gt;
&lt;td&gt;Insurance industry desktop applications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web Research&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Research&lt;/td&gt;
&lt;td&gt;Market analysis, competitive intelligence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each agent runs in its own terminal with its own context, its own memory, and its own &lt;code&gt;CLAUDE.md&lt;/code&gt; rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Communication: The Bus
&lt;/h2&gt;

&lt;p&gt;The agents talk through a dead-simple file-based message bus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.claude/bus/
├── inbox-boss.md
├── inbox-binance.md
├── inbox-freelance.md
├── inbox-home-assistant.md
├── inbox-xojo.md
├── inbox-web-research.md
├── send.sh
└── broadcast.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;send.sh&lt;/code&gt; writes a timestamped message to the target's inbox file. That's it. No Redis, no RabbitMQ, no WebSockets. Just files.&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# send.sh — one agent writes to another's inbox&lt;/span&gt;
&lt;span class="nv"&gt;FROM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="nv"&gt;TO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt; &lt;span class="nv"&gt;TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt; &lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$4&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="nv"&gt;$TYPE&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%H:%M&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;] from:&lt;/span&gt;&lt;span class="nv"&gt;$FROM&lt;/span&gt;&lt;span class="s2"&gt; | &lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.claude/bus/inbox-&lt;span class="nv"&gt;$TO&lt;/span&gt;.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent has a cron job (Claude Code's &lt;code&gt;CronCreate&lt;/code&gt;) polling its inbox every 3 minutes:&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="k"&gt;*&lt;/span&gt;/3 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; → &lt;span class="s2"&gt;"cat ~/.claude/bus/inbox-freelance.md — if content, execute, reply, clear"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an agent finds a message, it reads it, executes the task, sends a reply to the sender's inbox, and clears its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Conference
&lt;/h2&gt;

&lt;p&gt;Here's what tonight's conference actually looked like. Boss sent a broadcast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONFERENCE — КОЖЕН надсилає:
1. Who you are (1 line)
2. Status (3-5 lines)
3. Your #1 TOP INSIGHT
4. Your #1 TOP BLOCKER
5. Idea for another agent
6. What you need from the user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within 10 minutes, all agents responded. Real excerpts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Binance agent's insight:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"OBV Accumulation strategy confirmed: R:R 2.33 across 83 days. Futures executor doubles signal coverage — 80% of best signals are SHORT in bear markets."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Freelance agent's insight:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Upwork proposals are the shortest path to first $. 18 ready, 0 submitted. Everything else (tweets, articles, Reddit) is a long game."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The cross-pollination moment:&lt;/strong&gt;&lt;br&gt;
Freelance agent suggested packaging the trading bot as a Gumroad product. Binance agent sent its full feature list. Within 30 minutes, we had a product listing, a Dev.to article draft, and an Upwork proposal template — all generated from real data that one agent shared with another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works Better Than One Big Agent
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Context isolation
&lt;/h3&gt;

&lt;p&gt;Each agent's context window is dedicated to its project. The trading bot agent knows every test, every strategy parameter, every backtest result. It doesn't waste tokens on marketing copy or smart home configs.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Parallel execution
&lt;/h3&gt;

&lt;p&gt;While Freelance writes a Gumroad listing, Binance runs backtests, and Web Research scrapes job boards. They don't block each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Specialized memory
&lt;/h3&gt;

&lt;p&gt;Each agent has its own &lt;code&gt;memory/&lt;/code&gt; directory with project-specific knowledge. The Binance agent remembers which strategies failed and why. The Freelance agent remembers which Reddit subs need karma before posting. They don't pollute each other's context.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Boss as orchestrator
&lt;/h3&gt;

&lt;p&gt;The Boss agent doesn't code. It reads status from all agents, identifies bottlenecks, and delegates. Tonight it noticed that the trading bot's feature list could become a marketing asset — something neither the Binance nor Freelance agent would have seen independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Doesn't Work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; File-based polling at 3-minute intervals means conversations take 6+ minutes for a round-trip. If you need real-time coordination, this isn't it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context loss.&lt;/strong&gt; Each agent session eventually hits context limits and compresses. The &lt;code&gt;SESSION-HANDOFF.md&lt;/code&gt; pattern (mandatory update before session end) mitigates this, but knowledge still degrades over long sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single point of failure.&lt;/strong&gt; If Boss goes down, nobody delegates. The agents can still work independently, but coordination stops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User bottleneck.&lt;/strong&gt; Some actions still need human intervention — submitting Upwork proposals, uploading to Gumroad, posting on Reddit. The agents prepare everything, but the last mile is manual.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code CLI&lt;/strong&gt; — each instance in its own terminal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message bus&lt;/strong&gt; — bash scripts + markdown files in &lt;code&gt;~/.claude/bus/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cron polling&lt;/strong&gt; — &lt;code&gt;CronCreate&lt;/code&gt; (Claude Code built-in, session-scoped)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State persistence&lt;/strong&gt; — &lt;code&gt;SESSION-HANDOFF.md&lt;/code&gt; + &lt;code&gt;memory/&lt;/code&gt; per project&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broadcast&lt;/strong&gt; — &lt;code&gt;broadcast.sh&lt;/code&gt; writes to all inboxes simultaneously&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared rules&lt;/strong&gt; — &lt;code&gt;_coordination/shared/rules/&lt;/code&gt; read by all agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total infrastructure cost: $0. It's just files and Claude Code subscriptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Build Your Own
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create the bus directory&lt;/strong&gt; with an inbox file per agent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write &lt;code&gt;send.sh&lt;/code&gt;&lt;/strong&gt; — 4 lines of bash (shown above)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add inbox polling&lt;/strong&gt; to each agent's &lt;code&gt;CLAUDE.md&lt;/code&gt; or use &lt;code&gt;CronCreate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mandate SESSION-HANDOFF.md&lt;/strong&gt; — agents must document state before session end&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with 2 agents&lt;/strong&gt; — a Boss and one Worker. Add more as needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key insight: you don't need a framework. The simplest possible communication mechanism (files) works because Claude is smart enough to parse unstructured messages and act on them.&lt;/p&gt;

&lt;p&gt;The hard part isn't the infrastructure. It's designing the right agent boundaries — what each agent owns, what it doesn't, and when it should ask Boss for help vs. acting independently.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Running this system across 16 projects. Building in public — follow for more real architecture, no hype.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>architecture</category>
      <category>claude</category>
    </item>
    <item>
      <title>I Built a Crypto Bot That Actually Makes Money — Here's What I Learned</title>
      <dc:creator>DevForge Templates</dc:creator>
      <pubDate>Thu, 26 Mar 2026 07:49:22 +0000</pubDate>
      <link>https://dev.to/devforgedev/i-built-a-crypto-bot-that-actually-makes-money-heres-what-i-learned-3egc</link>
      <guid>https://dev.to/devforgedev/i-built-a-crypto-bot-that-actually-makes-money-heres-what-i-learned-3egc</guid>
      <description>&lt;p&gt;Everyone builds a crypto bot at some point. Most lose money. Mine didn't — but not for the reasons I expected.&lt;/p&gt;

&lt;p&gt;After 101 trades and 83 days of backtesting, here's what actually mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Strategy That Worked: OBV Accumulation
&lt;/h2&gt;

&lt;p&gt;I tried three different strategies before finding one that held up. Moving average crossovers? Too many false signals. RSI divergence? Works in textbooks, not in crypto's 24/7 volatility.&lt;/p&gt;

&lt;p&gt;What worked: &lt;strong&gt;On-Balance Volume (OBV) Accumulation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea is simple — track volume flow before price moves. When OBV is accumulating (smart money buying) while price is flat or dipping, that's your signal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_obv_accumulation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lookback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;obv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;obv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;obv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;volume&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="n"&gt;obv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# Accumulation = OBV rising while price is flat
&lt;/span&gt;    &lt;span class="n"&gt;obv_slope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linear_regression_slope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;lookback&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;
    &lt;span class="n"&gt;price_slope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;linear_regression_slope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;lookback&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="n"&gt;obv_slope&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price_slope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;  &lt;span class="c1"&gt;# e.g., 0.02
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches the "smart money" accumulation phase — institutional buying that precedes price moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers (Real, Not Cherry-Picked)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;101 backtested trades&lt;/strong&gt; across Jan–Mar 2026&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;37.6% win rate&lt;/strong&gt; — yes, fewer than half my trades win&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk-to-Reward: 2.33&lt;/strong&gt; — but winners are 2.3x bigger than losers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Max drawdown: $0.31 per trade&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That win rate looks bad until you do the math:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Expected value = (0.376 × 2.33) - (0.624 × 1.0) = 0.876 - 0.624 = +0.252
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every dollar risked returns $0.25 on average. That's the R:R discovery that changed everything for me — &lt;strong&gt;you don't need to be right most of the time, you need your wins to be bigger than your losses.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Got Wrong First
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: No momentum exhaustion check
&lt;/h3&gt;

&lt;p&gt;My first version would buy at the top of a pump because OBV was "accumulating." Added a momentum exhaustion gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_momentum_exhausted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;recent_gains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;recent_losses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;recent_losses&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# All gains, no pullback = exhausted
&lt;/span&gt;
    &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recent_gains&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;recent_losses&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;  &lt;span class="c1"&gt;# Overbought threshold
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single check eliminated my worst trades.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 2: Same position size for everything
&lt;/h3&gt;

&lt;p&gt;BNB and BTC don't move the same way. Volatility-adjusted sizing was the fix — smaller positions on volatile coins, bigger on stable ones. Sounds obvious in hindsight.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake 3: No cooldown between trades
&lt;/h3&gt;

&lt;p&gt;Without a per-symbol cooldown, the bot would enter and exit the same coin 3 times in an hour on noise. Added a 30-minute cooldown per symbol. Trade frequency dropped, profitability went up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Testing Obsession
&lt;/h2&gt;

&lt;p&gt;The bot has &lt;strong&gt;1,143 automated tests&lt;/strong&gt;. That sounds excessive for a side project. It's not.&lt;/p&gt;

&lt;p&gt;Crypto trades with real money. A bug isn't "the page looks weird" — it's "I just lost $500 at 3am while sleeping." Every safety mechanism has tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stop-loss caps per coin&lt;/li&gt;
&lt;li&gt;Anti-hedge protection (no conflicting long + short)&lt;/li&gt;
&lt;li&gt;Position recovery on restart&lt;/li&gt;
&lt;li&gt;Score velocity detection (blocks stale signals)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_anti_hedge_protection&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Bot must never hold LONG and SHORT on same symbol&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BTCUSDT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LONG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AntiHedgeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BTCUSDT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SHORT&lt;/span&gt;&lt;span class="sh"&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 building anything that touches money, test like your sleep depends on it — because it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.12&lt;/strong&gt; with asyncio (crypto = lots of concurrent WebSocket streams)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; with asyncpg (connection pooling for high-frequency inserts)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; dashboard for monitoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; for deployment (one command on any VPS)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram&lt;/strong&gt; bot for trade alerts on my phone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total infra cost: ~$5/month on a Hetzner ARM VPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with backtesting, not live trading.&lt;/strong&gt; I wasted 2 weeks on live trades with an unproven strategy. Build the backtester first.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Track MFE (Maximum Favorable Excursion).&lt;/strong&gt; This tells you how far winning trades go before reversing — critical for setting trailing stops from real data, not gut feeling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Futures from day one.&lt;/strong&gt; 80% of my best signals are SHORT opportunities in bear markets. Spot-only means you're blind to half the market.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't trust indicators in isolation.&lt;/strong&gt; OBV alone isn't enough. The scoring system combines multiple signals, and the accumulation pattern is just the strongest weight.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Is It Worth Building Your Own?
&lt;/h2&gt;

&lt;p&gt;If you want to learn: absolutely. I learned more about markets from building this bot than from any course or book.&lt;/p&gt;

&lt;p&gt;If you want passive income: maybe. The bot works, but crypto is crypto — strategies degrade, markets change, exchanges update APIs. It's not "set and forget."&lt;/p&gt;

&lt;p&gt;If you want guaranteed returns: no. And anyone promising that is lying.&lt;/p&gt;

&lt;p&gt;The real value is the framework — once you have a tested backtesting engine, a solid executor, and proper safety mechanisms, you can swap strategies as markets evolve.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building in public. Follow along for more real numbers, no hype.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>crypto</category>
      <category>trading</category>
      <category>docker</category>
    </item>
    <item>
      <title>I Built 12 Production React Templates with AI - Here Is What I Learned</title>
      <dc:creator>DevForge Templates</dc:creator>
      <pubDate>Tue, 24 Mar 2026 14:47:18 +0000</pubDate>
      <link>https://dev.to/devforgedev/i-built-12-production-react-templates-with-ai-here-is-what-i-learned-2hjd</link>
      <guid>https://dev.to/devforgedev/i-built-12-production-react-templates-with-ai-here-is-what-i-learned-2hjd</guid>
      <description>&lt;p&gt;Over the past 3 months, I built 12 production-ready fullstack templates — each with AI features, Docker deployment, and a real database. Not boilerplates with TODO comments. Actual apps with auth, payments, dashboards, and seed data.&lt;/p&gt;

&lt;p&gt;Here's what I learned, what worked, and what surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack (One Stack, Every Template)
&lt;/h2&gt;

&lt;p&gt;Every template runs the same stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React 19&lt;/strong&gt; + Vite 7 + Mantine 8 (frontend)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fastify 5&lt;/strong&gt; + Prisma 7 + PostgreSQL (backend)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zustand&lt;/strong&gt; + TanStack Query (state management)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zod&lt;/strong&gt; (shared validation — one schema, zero type drift)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt; + Traefik (deployment with auto-SSL)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why the same stack everywhere? Because consistency compounds. Every template shares the same patterns, the same folder structure, the same auth flow. If you learn one, you can navigate all twelve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 12 Templates
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Full-Stack Starter Kit&lt;/strong&gt; — Auth, dashboard, AI chat, Stripe. The base everything else builds on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AgentForge&lt;/strong&gt; — AI agent management dashboard. Agent registry, task queue, execution logs, CRON scheduling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CRM&lt;/strong&gt; — Contact management, pipeline stages, activity tracking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E-Commerce Dashboard&lt;/strong&gt; — Product management, orders, inventory, analytics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SaaS MVP&lt;/strong&gt; — Multi-tenant, subscription billing, usage tracking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Booking System&lt;/strong&gt; — Calendar, appointments, availability management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HR Platform&lt;/strong&gt; — Employee directory, leave management, onboarding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Portal&lt;/strong&gt; — Project tracking, file sharing, invoicing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Healthcare Dashboard&lt;/strong&gt; — Patient records, appointments, clinical data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restaurant Management&lt;/strong&gt; — Menu, orders, table management, kitchen display.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real Estate&lt;/strong&gt; — Property listings, virtual tours, lead management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Document Processor&lt;/strong&gt; — Upload, extract, summarize documents with AI.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Every Template Includes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JWT auth&lt;/strong&gt; with refresh token rotation + Google OAuth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI chat widget&lt;/strong&gt; (Claude/OpenAI API, mock mode included)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-based access&lt;/strong&gt; (admin, user, custom roles)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker deployment&lt;/strong&gt; — one &lt;code&gt;docker compose up&lt;/code&gt; for production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seed data&lt;/strong&gt; — realistic test data, not empty tables&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mock mode&lt;/strong&gt; — everything works without API keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters more than you'd think. If someone clones your template and has to configure 4 API keys before seeing anything work, most will give up. Mock mode means: clone, install, run. It just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building with AI: What Actually Helped
&lt;/h2&gt;

&lt;p&gt;I used Claude Code for about 70% of the development. Here's what worked:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Great for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating CRUD routes that follow established patterns&lt;/li&gt;
&lt;li&gt;Writing Prisma schemas and seed data&lt;/li&gt;
&lt;li&gt;Creating React components that match existing UI patterns&lt;/li&gt;
&lt;li&gt;Test scaffolding&lt;/li&gt;
&lt;li&gt;Docker configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not great for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Novel architectural decisions (still needs human thinking)&lt;/li&gt;
&lt;li&gt;Complex business logic with edge cases&lt;/li&gt;
&lt;li&gt;UI design choices (it follows patterns, doesn't create them)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key was CLAUDE.md — a file that defines project conventions, stack choices, and patterns. With good context, Claude generates code that fits. Without it, you get generic code that needs heavy editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment: Docker + Traefik
&lt;/h2&gt;

&lt;p&gt;Every template deploys the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.prod.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traefik handles SSL certificates automatically via Let's Encrypt. No nginx config files, no certbot cron jobs. Add a new template? Add Traefik labels. Done.&lt;/p&gt;

&lt;p&gt;Currently running 12 live demos on a single VPS, each with its own subdomain and auto-renewing SSL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;12 templates, all passing &lt;code&gt;pnpm build&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;~50,000 lines of TypeScript across all templates&lt;/li&gt;
&lt;li&gt;12 live demos running on Docker&lt;/li&gt;
&lt;li&gt;Same VPS, same Traefik instance&lt;/li&gt;
&lt;li&gt;Average template: 3,000-5,000 lines of code&lt;/li&gt;
&lt;li&gt;Time to deploy a new template: under 30 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Surprised Me
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mock mode was the highest-impact feature.&lt;/strong&gt; It took an extra day per template but made the difference between "interesting" and "I can actually use this."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Seed data sells templates.&lt;/strong&gt; An empty dashboard looks like a skeleton. A dashboard with realistic data looks like a product. Every template ships with 50-200 seed records.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One stack &amp;gt; variety.&lt;/strong&gt; I initially considered mixing Next.js, Remix, and Vite across templates. Staying with one stack made everything faster and more maintainable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI features are table stakes now.&lt;/strong&gt; In 2024, "has AI chatbot" was a differentiator. In 2026, templates without AI features feel incomplete.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Docker deployment is a selling point.&lt;/strong&gt; Many developers are tired of Vercel lock-in for dashboards that don't need edge functions. "Self-hosted with Docker" resonates.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;All templates are live on Gumroad with a 30% launch discount (code: LAUNCH30). Each comes with full TypeScript source, Docker config, seed data, and MIT license.&lt;/p&gt;

&lt;p&gt;Live demos and details: devforgetemplates.gumroad.com&lt;/p&gt;

&lt;p&gt;I'm also offering custom builds — if you need a template adapted for your specific use case, that's my fastest turnaround since the foundation is already built.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full disclosure: I built these templates and sell them. This article is my launch announcement, but I tried to make the technical content genuinely useful regardless. Happy to answer questions about the stack, architecture, or deployment setup in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>docker</category>
      <category>ai</category>
    </item>
    <item>
      <title>7 Claudes Working Together: How I Built a Multi-Agent Coordination System</title>
      <dc:creator>DevForge Templates</dc:creator>
      <pubDate>Tue, 24 Mar 2026 14:33:05 +0000</pubDate>
      <link>https://dev.to/devforgedev/7-claudes-working-together-how-i-built-a-multi-agent-coordination-system-3ogi</link>
      <guid>https://dev.to/devforgedev/7-claudes-working-together-how-i-built-a-multi-agent-coordination-system-3ogi</guid>
      <description>&lt;p&gt;What happens when you stop thinking of Claude as a chatbot and start treating it as infrastructure?&lt;/p&gt;

&lt;p&gt;I run 7 Claude instances simultaneously — each with a specialized role, shared rules, and a message bus for inter-agent communication. Here's how the system works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I manage 16+ projects across different domains: web templates, marketing automation, infrastructure, research. Context-switching between projects meant losing state constantly. Each conversation with Claude started from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: Boss + Workers
&lt;/h2&gt;

&lt;p&gt;The system has two layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boss Claude&lt;/strong&gt; — coordinates everything. Reads project states, makes architectural decisions, delegates tasks, monitors progress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worker Claudes&lt;/strong&gt; — specialized agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Freelance Claude: marketing, Gumroad products, Upwork proposals&lt;/li&gt;
&lt;li&gt;Web Research Claude: market scans, competitor analysis&lt;/li&gt;
&lt;li&gt;Infrastructure Claude: Docker deployments, VPS management&lt;/li&gt;
&lt;li&gt;And 4 more for specific project domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each worker has its own CLAUDE.md with scoped rules, its own &lt;code&gt;.claude/rules/&lt;/code&gt; directory, and its own session handoff files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Communication: The Message Bus
&lt;/h2&gt;

&lt;p&gt;Agents communicate through a file-based message bus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/.claude/bus/
├── inbox-boss.md
├── inbox-freelance.md
├── inbox-web-research.md
├── broadcast.md
└── send.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To send a message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/.claude/bus/send.sh freelance boss QUESTION &lt;span class="s2"&gt;"What's the VPS IP for the new project?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Boss reads the message, responds, and writes the reply to the sender's inbox. Each agent checks its inbox via a CronCreate job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared Rules
&lt;/h2&gt;

&lt;p&gt;All agents follow core rules stored in a shared location:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;_coordination/shared/rules/
├── core.md        &lt;span class="c"&gt;# 7 universal rules&lt;/span&gt;
├── errors.md      &lt;span class="c"&gt;# Shared error log&lt;/span&gt;
└── user-feedback.md  &lt;span class="c"&gt;# User corrections&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules include: think deeper before acting, never silently guess, communicate progress (DOING/FOUND/NEXT), learn from mistakes, and context budget management.&lt;/p&gt;

&lt;p&gt;When one agent discovers a bug or limitation, it records it in &lt;code&gt;errors.md&lt;/code&gt;. Every other agent reads this at session start — so a mistake happens once across the entire system, not once per agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  State Management
&lt;/h2&gt;

&lt;p&gt;Each agent maintains its own state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;_coordination/agents/freelance/
├── state.md    &lt;span class="c"&gt;# Current status, what I'm working on&lt;/span&gt;
├── history.md  &lt;span class="c"&gt;# Append-only decision log&lt;/span&gt;
└── charter.md  &lt;span class="c"&gt;# Agent's role and responsibilities&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before session end, every agent updates its state. The next session (or another agent checking in) gets full context without re-explaining anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooks &amp;gt; CLAUDE.md
&lt;/h2&gt;

&lt;p&gt;The most important lesson: use hooks for hard rules, not CLAUDE.md instructions.&lt;/p&gt;

&lt;p&gt;CLAUDE.md instructions can be forgotten in long sessions as context grows. Hooks fire mechanically every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"UserPromptSubmit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cat ~/.claude/bus/inbox-freelance.md"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hook checks the inbox on every user message — impossible to forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  CronCreate for Background Tasks
&lt;/h2&gt;

&lt;p&gt;Each agent sets up recurring tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CronCreate: */15 * * * * — Check inbox
CronCreate: 17 * * * * — Check Gumroad sales
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Boss broadcasts to all agents when shared rules change or when cross-project decisions are made.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Specialization beats generalization.&lt;/strong&gt; A Claude with 60 lines of focused CLAUDE.md outperforms one with 500 lines of "know everything" context.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;File-based communication is surprisingly robust.&lt;/strong&gt; No databases, no APIs, no infrastructure to maintain. Just markdown files and shell scripts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Session handoff files are essential.&lt;/strong&gt; Without them, every new session starts cold. With them, agents resume exactly where they left off.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Shared error logs prevent repeated mistakes.&lt;/strong&gt; One agent hits a platform limitation, all agents learn about it immediately.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Doesn't Work
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real-time coordination is slow.&lt;/strong&gt; File-based messaging has inherent latency (cron interval). If you need sub-second agent coordination, this isn't the architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context budget is real.&lt;/strong&gt; ETH Zurich research confirmed: verbose CLAUDE.md files actually reduce performance. Keep each agent's context lean.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debugging cross-agent issues is hard.&lt;/strong&gt; When Boss delegates to Freelance, which delegates research to Web Research — tracing a failure through 3 agents requires reading 3 separate logs.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;7 active Claude instances&lt;/li&gt;
&lt;li&gt;16 projects managed&lt;/li&gt;
&lt;li&gt;~50 messages/day across the bus&lt;/li&gt;
&lt;li&gt;State files updated 20+ times/day&lt;/li&gt;
&lt;li&gt;Shared rules: 7 core rules, 15+ error entries&lt;/li&gt;
&lt;li&gt;Total coordination overhead: ~5% of each session&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Would I Recommend This?
&lt;/h2&gt;

&lt;p&gt;For a single project? No — overkill. Use one Claude with a good CLAUDE.md.&lt;/p&gt;

&lt;p&gt;For managing multiple projects with different contexts? Absolutely. The specialization alone is worth it. And the shared learning (errors.md, user-feedback.md) means the entire system gets smarter every day.&lt;/p&gt;

&lt;p&gt;The key insight: Claude works best when it has focused context and clear boundaries. Seven specialized Claudes &amp;gt; one Claude trying to know everything.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>architecture</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>How I Built a Crypto Trading Bot with Claude Code in 3 Weeks</title>
      <dc:creator>DevForge Templates</dc:creator>
      <pubDate>Tue, 24 Mar 2026 14:33:03 +0000</pubDate>
      <link>https://dev.to/devforgedev/how-i-built-a-crypto-trading-bot-with-claude-code-in-3-weeks-165o</link>
      <guid>https://dev.to/devforgedev/how-i-built-a-crypto-trading-bot-with-claude-code-in-3-weeks-165o</guid>
      <description>&lt;p&gt;Building a crypto trading bot sounds glamorous until you're debugging why your scoring algorithm triggered a buy at 3 AM on a coin that dropped 40% by morning.&lt;/p&gt;

&lt;p&gt;Here's what I learned building a production crypto trading system with Claude Code as my primary development tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The bot has 22 scoring components that analyze different market signals: volume spikes, price momentum, RSI divergence, order book depth, social sentiment, and more. Each component returns a weighted score between -1 and 1. The aggregate score determines buy/sell/hold.&lt;/p&gt;

&lt;p&gt;The backend runs on Fastify 5 with TypeScript. PostgreSQL stores historical scores and trades. A React dashboard shows real-time portfolio status, active signals, and historical performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Claude Code Changed Everything
&lt;/h2&gt;

&lt;p&gt;Before Claude Code, I was context-switching between docs, Stack Overflow, and my editor constantly. With Claude Code, the workflow became:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Describe the scoring component I need&lt;/li&gt;
&lt;li&gt;Claude reads the existing codebase (CLAUDE.md + shared types)&lt;/li&gt;
&lt;li&gt;It generates the component following the established pattern&lt;/li&gt;
&lt;li&gt;I review, test, iterate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key was the CLAUDE.md file. I defined the project structure, naming conventions, and the scoring component interface once. Every new component followed the same pattern automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  22 Scoring Components in Detail
&lt;/h2&gt;

&lt;p&gt;Each component is a standalone module with the same interface:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ScoringComponent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;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;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MarketData&lt;/span&gt;&lt;span class="p"&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="nx"&gt;Score&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;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Score&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// -1 to 1&lt;/span&gt;
  &lt;span class="nl"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0 to 1&lt;/span&gt;
  &lt;span class="nl"&gt;reasoning&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;p&gt;The reasoning field was critical for debugging. When a trade goes wrong, you can trace back through each component's reasoning to understand what the aggregate model "thought."&lt;/p&gt;

&lt;p&gt;Some components that worked well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Volume-Price Divergence&lt;/strong&gt;: When volume spikes but price stays flat, something is about to move&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Timeframe RSI&lt;/strong&gt;: RSI on 5m, 15m, 1h, 4h — agreement across timeframes = strong signal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order Book Imbalance&lt;/strong&gt;: &amp;gt;3:1 bid/ask ratio at key levels often precedes moves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Components that looked good in backtests but failed live:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Social Sentiment&lt;/strong&gt;: Too noisy, too slow. By the time sentiment shifts measurably, the move already happened&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlation Clustering&lt;/strong&gt;: BTC correlation breaks down exactly when you need it most — during crashes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing: 1000+ Tests
&lt;/h2&gt;

&lt;p&gt;Every scoring component has unit tests with historical data. But the real value came from integration tests that replay entire market days and verify the aggregate score matches expected behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;pnpm test
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;1,047 tests passing
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Coverage: 89% statements, 94% branches on scoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code generated most of the test scaffolding. I described the edge cases, it wrote the tests. Saved days of tedious work.&lt;/p&gt;

&lt;h2&gt;
  
  
  15 Deploys Per Day
&lt;/h2&gt;

&lt;p&gt;With Docker + Traefik on a VPS, deploying is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rsync &lt;span class="nt"&gt;-az&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; vps:~/crypto-bot/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ssh vps &lt;span class="s1"&gt;'cd crypto-bot &amp;amp;&amp;amp; docker compose up -d --build'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During active development, I was deploying 10-15 times per day. Hot-fixing a scoring weight, deploying, watching the next few signals, adjusting. The tight feedback loop was essential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Backtesting lies.&lt;/strong&gt; Every backtest has survivorship bias. Your bot will encounter market conditions that don't exist in your test data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with paper trading.&lt;/strong&gt; Run your bot with zero real money for at least 2 weeks. Track what it would have done.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Position sizing matters more than signal quality.&lt;/strong&gt; A mediocre signal with good risk management beats a great signal with poor position sizing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Claude Code excels at repetitive patterns.&lt;/strong&gt; 22 components that all follow the same interface? Perfect for AI-assisted development. Novel algorithm design? Still needs human thinking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CLAUDE.md is your best investment.&lt;/strong&gt; 30 minutes writing a good CLAUDE.md saves hours of correcting AI-generated code that doesn't match your patterns.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Fastify 5 + TypeScript + Prisma 7 + PostgreSQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: React 19 + Vite + Mantine (dashboard)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infra&lt;/strong&gt;: Docker + Traefik + VPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Dev&lt;/strong&gt;: Claude Code with custom CLAUDE.md&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing&lt;/strong&gt;: Vitest, 1000+ tests&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Would I Do It Again?
&lt;/h2&gt;

&lt;p&gt;Yes, but I'd skip the social sentiment component entirely and invest that time in better position sizing logic. The scoring is 30% of the system. Risk management is 70%.&lt;/p&gt;

&lt;p&gt;And I'd use Claude Code from day one instead of starting manually and migrating halfway through. The CLAUDE.md-driven workflow is genuinely faster for structured, pattern-heavy codebases.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>crypto</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>One Schema, Zero Drift: How Zod Keeps My Frontend and Backend in Sync</title>
      <dc:creator>DevForge Templates</dc:creator>
      <pubDate>Mon, 23 Mar 2026 16:18:59 +0000</pubDate>
      <link>https://dev.to/devforgedev/one-schema-zero-drift-how-zod-keeps-my-frontend-and-backend-in-sync-2glj</link>
      <guid>https://dev.to/devforgedev/one-schema-zero-drift-how-zod-keeps-my-frontend-and-backend-in-sync-2glj</guid>
      <description>&lt;p&gt;TypeScript catches a lot of bugs. But it has a blind spot: the network boundary. Your server returns &lt;code&gt;{ createdAt: string }&lt;/code&gt; instead of &lt;code&gt;{ created_at: string }&lt;/code&gt;, and TypeScript won't say a word. You'll find out at runtime, when the UI renders "undefined" where a date should be.&lt;/p&gt;

&lt;p&gt;This happens because TypeScript types are erased at compile time. They describe what your code &lt;em&gt;expects&lt;/em&gt;, not what actually arrives over the wire. Your frontend type says &lt;code&gt;User&lt;/code&gt;, but the API could return anything -- a different shape, extra fields, missing fields, wrong types. TypeScript just trusts you.&lt;/p&gt;

&lt;p&gt;Zod fixes this by making validation and types the same thing. You define a schema once, in a shared folder, and both sides of the app use it. The server validates incoming requests against it. The client validates forms against it. TypeScript infers types from it. One source of truth, enforced at runtime and compile time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shared Schema
&lt;/h2&gt;

&lt;p&gt;Start with a &lt;code&gt;/shared&lt;/code&gt; folder at the project root. Both server and client import from here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/shared/schemas/user.ts
/server/routes/users.ts      ← imports from shared
/client/features/users/       ← imports from shared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a user creation schema:&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="c1"&gt;// shared/schemas/user.ts&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;z&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="s2"&gt;zod&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;CreateUserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid email address&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Name must be at least 2 characters&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&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="s2"&gt;member&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="s2"&gt;viewer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;avatarUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&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;type&lt;/span&gt; &lt;span class="nx"&gt;CreateUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;CreateUserSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// → { email: string; name: string; role: "admin" | "member" | "viewer"; avatarUrl?: string }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;z.infer&lt;/code&gt; extracts the TypeScript type from the schema. You never write the type by hand. If you add a field to the schema, the type updates automatically, and every file that imports &lt;code&gt;CreateUser&lt;/code&gt; gets type-checked against the new shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server: Fastify Request Validation
&lt;/h2&gt;

&lt;p&gt;On the backend, use the schema to validate incoming request bodies. Invalid data gets rejected before your route handler ever runs:&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="c1"&gt;// server/routes/users.ts&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;CreateUserSchema&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="s2"&gt;../../shared/schemas/user.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;fastify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/users&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&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="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;CreateUserSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Validation failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&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="c1"&gt;// parsed.data is fully typed as CreateUser&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&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="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;avatarUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avatarUrl&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;return&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;safeParse&lt;/code&gt; returns either &lt;code&gt;{ success: true, data: CreateUser }&lt;/code&gt; or &lt;code&gt;{ success: false, error: ZodError }&lt;/code&gt;. No try/catch needed. The data inside &lt;code&gt;parsed.data&lt;/code&gt; is guaranteed to match the schema -- not just typed, actually validated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client: React Hook Form + Mantine
&lt;/h2&gt;

&lt;p&gt;The same schema drives form validation on the frontend. With &lt;code&gt;@hookform/resolvers/zod&lt;/code&gt;, you connect the Zod schema directly to React Hook Form:&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="c1"&gt;// client/features/users/CreateUserForm.tsx&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;useForm&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="s2"&gt;react-hook-form&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;zodResolver&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="s2"&gt;@hookform/resolvers/zod&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;TextInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Button&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="s2"&gt;@mantine/core&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;CreateUserSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CreateUser&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="s2"&gt;../../../shared/schemas/user.js&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;function&lt;/span&gt; &lt;span class="nf"&gt;CreateUserForm&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;register&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;formState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useForm&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CreateUser&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;resolver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;zodResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CreateUserSchema&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;onSubmit&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateUser&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="c1"&gt;// data is validated and typed -- matches exactly what the server expects&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/users&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="s2"&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="s2"&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;data&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onSubmit&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;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TextInput&lt;/span&gt;
        &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
        &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errors&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;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TextInput&lt;/span&gt;
        &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
        &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Select&lt;/span&gt;
        &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&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="s2"&gt;member&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="s2"&gt;viewer&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="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
        &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Create&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&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;The error messages you defined in the schema (&lt;code&gt;"Invalid email address"&lt;/code&gt;, &lt;code&gt;"Name must be at least 2 characters"&lt;/code&gt;) show up directly in the form. No duplication. If you change a validation rule in the schema, both the server rejection message and the client form error update at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When You Change a Field
&lt;/h2&gt;

&lt;p&gt;Say you rename &lt;code&gt;role&lt;/code&gt; to &lt;code&gt;accessLevel&lt;/code&gt; in the schema:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CreateUserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid email address&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Name must be at least 2 characters&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;accessLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&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="s2"&gt;member&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="s2"&gt;viewer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;  &lt;span class="c1"&gt;// renamed&lt;/span&gt;
  &lt;span class="na"&gt;avatarUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&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;Run &lt;code&gt;tsc&lt;/code&gt; and you'll immediately see errors in every file that references &lt;code&gt;role&lt;/code&gt; -- the server route, the form component, any tests. Nothing slips through. This is the whole point: the schema is the contract, and TypeScript enforces it at compile time across both sides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  .transform(): Reshape Data at Parse Time
&lt;/h3&gt;

&lt;p&gt;Normalize data as it enters the system. The schema describes both the input and the output:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CreateUserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;transform&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="o"&gt;=&amp;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;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&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="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&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="s2"&gt;member&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="s2"&gt;viewer&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;parsed.data.email&lt;/code&gt; is always lowercase and trimmed, regardless of what the client sends. The transform runs during &lt;code&gt;safeParse&lt;/code&gt;, so your route handler never deals with messy input.&lt;/p&gt;

&lt;h3&gt;
  
  
  .refine(): Custom Validation Logic
&lt;/h3&gt;

&lt;p&gt;For rules that go beyond basic type checks:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DateRangeSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&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;refine&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;data&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endDate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;End date must be after start date&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;path&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="s2"&gt;endDate&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;path&lt;/code&gt; option tells React Hook Form which field to attach the error to. The form shows the message under the end date input without any extra wiring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Discriminated Unions for API Responses
&lt;/h3&gt;

&lt;p&gt;Type-safe API responses where the structure depends on a status field:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ApiResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;discriminatedUnion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&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="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&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;type&lt;/span&gt; &lt;span class="nx"&gt;ApiResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;ApiResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// TypeScript knows: if status === "success", then .data exists&lt;/span&gt;
&lt;span class="c1"&gt;// if status === "error", then .message and .code exist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern eliminates &lt;code&gt;if (response.data)&lt;/code&gt; guessing. You check &lt;code&gt;status&lt;/code&gt;, and TypeScript narrows the type automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Handling: Zod Errors to User-Friendly Messages
&lt;/h2&gt;

&lt;p&gt;Zod's error structure is designed for programmatic consumption. Each issue includes a &lt;code&gt;path&lt;/code&gt; (which field), a &lt;code&gt;code&lt;/code&gt; (what went wrong), and a &lt;code&gt;message&lt;/code&gt; (human-readable). You can map these to form fields directly:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ZodError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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;&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="na"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="k"&gt;for &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;issue&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&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;field&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&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;formatted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;return&lt;/span&gt; &lt;span class="nx"&gt;formatted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Server returns: { issues: [{ path: ["email"], message: "Invalid email address", code: "invalid_string" }] }&lt;/span&gt;
&lt;span class="c1"&gt;// formatErrors turns it into: { email: "Invalid email address" }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means your server's 400 response can be displayed directly in the form -- the same error format, whether validation runs client-side or server-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;Here's how the shared schemas fit into a fullstack monorepo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shared/
  schemas/
    user.ts         # CreateUserSchema, UpdateUserSchema, UserSchema
    project.ts      # CreateProjectSchema, ProjectFilters, etc.
    auth.ts         # LoginSchema, RegisterSchema
    common.ts       # PaginationSchema, SortSchema, IdParam
  index.ts          # Re-exports everything

server/
  routes/
    users.ts        # imports CreateUserSchema for validation
    projects.ts

client/
  features/
    users/
      CreateUserForm.tsx   # imports CreateUserSchema for form validation
      UserList.tsx
    projects/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;shared/schemas/common.ts&lt;/code&gt; is where you put reusable pieces:&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="c1"&gt;// shared/schemas/common.ts&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;PaginationSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&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;IdParamSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;cuid&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;Every route that accepts pagination params uses the same schema. Every form that includes an ID validates it the same way. One change propagates everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Eliminates
&lt;/h2&gt;

&lt;p&gt;The pattern removes an entire class of bugs that TypeScript alone can't catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Field renames&lt;/strong&gt; that break one side but not the other&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type mismatches&lt;/strong&gt; between what the API sends and what the UI expects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicated validation&lt;/strong&gt; with rules that drift out of sync&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime surprises&lt;/strong&gt; from unvalidated external data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost is one extra dependency and a &lt;code&gt;shared/&lt;/code&gt; folder. The return is that your frontend and backend are provably talking about the same data shapes -- checked by the compiler, enforced at runtime, defined in one place.&lt;/p&gt;

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