<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sharang Parnerkar</title>
    <description>The latest articles on DEV Community by Sharang Parnerkar (@mighty840).</description>
    <link>https://dev.to/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%2F73fff9cf-338c-4847-8f7d-364eb5f061e5.jpeg</url>
      <title>DEV Community: Sharang Parnerkar</title>
      <link>https://dev.to/mighty840</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mighty840"/>
    <language>en</language>
    <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://dev.to/mighty840/i-built-a-container-orchestrator-in-rust-because-kubernetes-was-too-much-and-coolify-wasnt-enough-4hj7</link>
      <guid>https://dev.to/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://dev.to/mighty840/design-decisions-behind-kitchenasty-an-open-source-restaurant-management-system-650</link>
      <guid>https://dev.to/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://dev.to/mighty840/i-built-an-open-source-alternative-to-toast-and-square-for-restaurant-management-3mdf</link>
      <guid>https://dev.to/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>
