<?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: Panth Patel</title>
    <description>The latest articles on DEV Community by Panth Patel (@panthpatel).</description>
    <link>https://dev.to/panthpatel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4143534%2F3561e361-6ed6-41fc-8bad-811d1a0a9b77.png</url>
      <title>DEV Community: Panth Patel</title>
      <link>https://dev.to/panthpatel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/panthpatel"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>Panth Patel</dc:creator>
      <pubDate>Tue, 27 Oct 2026 03:02:34 +0000</pubDate>
      <link>https://dev.to/panthpatel/-1n2j</link>
      <guid>https://dev.to/panthpatel/-1n2j</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/gde/postmortem-the-tuesday-our-ai-bill-had-no-owner-34pd" class="crayons-story__hidden-navigation-link"&gt;Postmortem: The Tuesday Our AI Bill Had No Owner&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/gde/postmortem-the-tuesday-our-ai-bill-had-no-owner-34pd" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Unlabeled tokens create hidden retry loops&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/gde"&gt;
            &lt;img alt="Google Developer Experts logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F11939%2Fe3080d5b-ecde-42a8-b089-bafecc31fa97.png" class="crayons-logo__image" width="800" height="800"&gt;
          &lt;/a&gt;

          &lt;a href="/karthidec" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F99mvlsfu5tfj9m7ku25d.png" alt="karthidec profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/karthidec" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Karthigayan Devan
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Karthigayan Devan
                
                
              
              &lt;div id="story-author-preview-content-4915028" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/karthidec" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F99mvlsfu5tfj9m7ku25d.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Karthigayan Devan&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/gde" class="crayons-story__secondary fw-medium"&gt;Google Developer Experts&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/gde/postmortem-the-tuesday-our-ai-bill-had-no-owner-34pd" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/gde/postmortem-the-tuesday-our-ai-bill-had-no-owner-34pd" id="article-link-4915028"&gt;
          Postmortem: The Tuesday Our AI Bill Had No Owner
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/vertexai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;vertexai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/sre"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;sre&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/gde/postmortem-the-tuesday-our-ai-bill-had-no-owner-34pd" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;14&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/gde/postmortem-the-tuesday-our-ai-bill-had-no-owner-34pd#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              8&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            14 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>management</category>
      <category>productivity</category>
      <category>startup</category>
    </item>
    <item>
      <title>944 lines that turn a ticket into a headless Claude Code session</title>
      <dc:creator>Panth Patel</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:00:30 +0000</pubDate>
      <link>https://dev.to/panthpatel/944-lines-that-turn-a-ticket-into-a-headless-claude-code-session-4aag</link>
      <guid>https://dev.to/panthpatel/944-lines-that-turn-a-ticket-into-a-headless-claude-code-session-4aag</guid>
      <description>&lt;p&gt;I hand work to Claude Code through tickets, not a terminal. A 944-line orchestrator with zero dependencies turns each ticket into its own headless Claude Code session, and on 27 September I open-sourced it together with the tracker it works in: &lt;a href="https://github.com/Panth977/ticket-tracker" rel="noopener noreferrer"&gt;ticket-tracker&lt;/a&gt;, MIT.&lt;/p&gt;

&lt;p&gt;I'm Panth, and I lead the software team at Oizom. I dropped out before the AI wave, because I figured the engineering was in solving problems in the real world, and my college made me choose between attendance and real-world applications. This post is about how the orchestrator works and why it is shaped the way it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why tickets and not a terminal
&lt;/h2&gt;

&lt;p&gt;I wanted to give my VM work from my phone, while travelling or waiting in a queue, with no laptop. The first version was a terminal in an app. It grew into a room full of agents, and three things went wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I got attached to one long-running agent, and starting fresh ones was a chore.&lt;/li&gt;
&lt;li&gt;The app was a door into the VM. If someone broke in, I would spend a long time cleaning up.&lt;/li&gt;
&lt;li&gt;When the VM lost internet, I couldn't even queue a prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tickets fixed all three. Every ticket gets its own session, so there is no single agent to get attached to. The orchestrator runs on the machine and pulls work from the tracker, so nothing has to connect into that machine. The tracker is hosted apart from it, so I can raise tickets while the VM is offline, and my VM and my PC can take work from the same board.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop
&lt;/h2&gt;

&lt;p&gt;An agent is a board member: a profile, a system prompt and a token that acts as that agent on one board. Assign a ticket to it and move the ticket to To do. From there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;To do  ──orch claims──▶ In progress ──agent reports "review"──▶ Review ──owner──▶ Done
                            │  ▲
           agent asks a     │  │ the answer (form or a comment) resumes
           question ────────▼  │ the same session
                          waiting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orchestrator claims the ticket and starts &lt;code&gt;claude -p&lt;/code&gt; in the repo the workspace points at, with the tracker's MCP tools. The agent reads the ticket and its thread, then publishes its plan as a task list on the ticket and marks each item as it goes.&lt;/p&gt;

&lt;p&gt;When it needs a decision, it calls &lt;code&gt;ask_question&lt;/code&gt;, which puts a form card in the thread, and ends its turn with &lt;code&gt;status: "waiting"&lt;/code&gt;. It does not poll. The answer, or any comment in the thread, starts the next turn with &lt;code&gt;--resume&lt;/code&gt; on the same session, so the agent picks up with its whole history. Moving the ticket back to To do resumes it as rework. Unassigning it, or moving it anywhere else while the agent runs, stops the agent.&lt;/p&gt;

&lt;p&gt;The brief every agent gets says it in two lines:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You run headless in [the repo] and nobody is watching your terminal. The owner only sees the ticket, so everything you want them to see goes on the ticket.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The orchestrator owns the ticket's state
&lt;/h2&gt;

&lt;p&gt;The agent gets the tracker's MCP tools, minus the ones that decide where the ticket stands:&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;// What only the orch may do: the stage, the heartbeat, the inbox, assignment.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DISALLOWED&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;move_ticket&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;update_ticket&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;assign_ticket&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;create_ticket&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;heartbeat&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;get_events&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;ack_events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`mcp__tm__&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;t&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;join&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent does the work and reports &lt;code&gt;review&lt;/code&gt;, &lt;code&gt;waiting&lt;/code&gt; or &lt;code&gt;blocked&lt;/code&gt;. The orchestrator moves the ticket, beats, reads the event inbox and posts the receipt. So a ticket only reaches Review after the orchestrator has seen the agent report it done.&lt;/p&gt;

&lt;p&gt;The receipt is one message per turn with what the turn cost. Claude Code's &lt;code&gt;total_cost_usd&lt;/code&gt; is cumulative across &lt;code&gt;--resume&lt;/code&gt;, so the orchestrator stores the last total and posts the difference. Because the agent pushes the cost of each turn, you can see where the money actually went — which ticket, which turn — instead of one number at the end of the month. One ticket of mine reached $402 over nine runs before the app showed any of it; the full story of that fix is in &lt;a href="https://panth.vardayinitech.in/blog/cost-per-turn-headless-claude-code" rel="noopener noreferrer"&gt;$402 on one ticket&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Health from a heartbeat
&lt;/h2&gt;

&lt;p&gt;The orchestrator beats once a minute while an agent runs, and every fifth beat it re-reads the task list, so the ticket's dot shows progress like &lt;code&gt;3/12 · Running tests&lt;/code&gt;. The board never stores "this agent is dead". It derives health from the last status and the time since the last beat, in one function, recomputed every 15 seconds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Health&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;working&lt;/td&gt;
&lt;td&gt;state is working and the last beat is under 75 s old&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stale&lt;/td&gt;
&lt;td&gt;state is working and no beat for 75 s: the orchestrator may have died&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;idle&lt;/td&gt;
&lt;td&gt;waiting for an answer, or stopped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;done&lt;/td&gt;
&lt;td&gt;finished, ready for review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;error&lt;/td&gt;
&lt;td&gt;stopped with an error, for example a timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;no status yet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;75 seconds is one missed beat plus a little slack. Only "working" can go stale: idle, done and error are statements that stay true until the next beat. After five minutes of silence the agent's owner gets one notification per silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  One command layer
&lt;/h2&gt;

&lt;p&gt;The app, the REST API (&lt;code&gt;/v1&lt;/code&gt;, OpenAPI 3.1) and the MCP server (&lt;code&gt;/mcp&lt;/code&gt;) all run the same commands. Nothing an agent can do is different from what a person can do, and every change is attributed, for example "Owner via Claude (MCP)". Tokens are scoped to one board and act either as you or as one of your agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  What running it takes
&lt;/h2&gt;

&lt;p&gt;Everything runs on one Firebase project: Firestore, Functions, Storage, Realtime Database, Auth and Hosting. "Self-hosted" here means your own Firebase project on the Blaze plan, not a Docker image. Locally, &lt;code&gt;pnpm dev&lt;/code&gt; starts the emulators, seeds demo data and serves the app (Node 22, pnpm 9, Java 21 and the Firebase CLI).&lt;/p&gt;

&lt;p&gt;Firebase, because the tracker had to live apart from the machines doing the work: if the VM is offline or the agents are down, I can still raise tickets. From there the app does live reads straight from Firestore, Auth and push come with it, and the orchestrator wakes on a Realtime Database revision stream, so when an agent comes back it picks up where it left off.&lt;/p&gt;

&lt;p&gt;A workspace is a folder with &lt;code&gt;workspace.json&lt;/code&gt; (repo, board, agent, how many agents at once), a &lt;code&gt;.env&lt;/code&gt; with a Worker token for that agent, and &lt;code&gt;brief.md&lt;/code&gt; with the project's rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node workspaces/ws.mjs new myproject &lt;span class="nt"&gt;--repo&lt;/span&gt; ~/src/myproject &lt;span class="nt"&gt;--board&lt;/span&gt; MYP &lt;span class="nt"&gt;--agent&lt;/span&gt; &lt;span class="s2"&gt;"Claude MyProject"&lt;/span&gt;
&lt;span class="c"&gt;# mint a Worker token for that agent in the app (Account › Tokens), put it in workspaces/myproject/.env&lt;/span&gt;
node workspaces/ws.mjs up
node workspaces/ws.mjs status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents run unattended in your working tree, with &lt;code&gt;permissionMode&lt;/code&gt; from &lt;code&gt;workspace.json&lt;/code&gt;: &lt;code&gt;acceptEdits&lt;/code&gt; in the template, &lt;code&gt;bypassPermissions&lt;/code&gt; only if you trust the brief and the repo. Read &lt;code&gt;workspaces/lib/brief.md&lt;/code&gt; before you enable one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is not in it
&lt;/h2&gt;

&lt;p&gt;At work I've written about per-ticket stacks: a branch in every repo, its own services, its own copy of the dev database and a preview URL per ticket. None of that is in this repo. That code is specific to Envizom and proprietary. Here the agent works in the repo you point it at.&lt;/p&gt;

&lt;p&gt;The repo is &lt;a href="https://github.com/Panth977/ticket-tracker" rel="noopener noreferrer"&gt;github.com/Panth977/ticket-tracker&lt;/a&gt;; &lt;code&gt;workspaces/README.md&lt;/code&gt; has the loop.&lt;/p&gt;

&lt;p&gt;If you run Claude Code headless, where does its question go when it needs a decision, and who answers it?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>opensource</category>
      <category>ai</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>Replacing Node-RED with a 10 MB browser editor and a start.sh</title>
      <dc:creator>Panth Patel</dc:creator>
      <pubDate>Sun, 25 Oct 2026 13:01:09 +0000</pubDate>
      <link>https://dev.to/panthpatel/replacing-node-red-with-a-10-mb-browser-editor-and-a-startsh-1mh0</link>
      <guid>https://dev.to/panthpatel/replacing-node-red-with-a-10-mb-browser-editor-and-a-startsh-1mh0</guid>
      <description>&lt;p&gt;A client disconnected our server from the internet for security, and the integration for that client still had to be written on it.&lt;/p&gt;

&lt;p&gt;I'm Panth, and I lead the software team at Oizom, where we build Envizom, an air quality platform that many clients run on their own servers. Every one of those deployments needs a bit of custom work: pull data from somewhere, or expose an API in a schema the client gives us for a public display. That work can't go into our production code. This post is about where it lives now: &lt;a href="https://github.com/Panth977/code-shell" rel="noopener noreferrer"&gt;codeshell&lt;/a&gt;, a small browser workspace I built and open-sourced under Apache-2.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node-RED first
&lt;/h2&gt;

&lt;p&gt;Node-RED was the default for these integrations before I joined. It is fragile for this kind of work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An async for loop is hard to write.&lt;/li&gt;
&lt;li&gt;Five API calls with something equivalent to &lt;code&gt;Promise.all&lt;/code&gt; is close to rocket science.&lt;/li&gt;
&lt;li&gt;Debugging is the worst part.&lt;/li&gt;
&lt;li&gt;If there is a JavaScript library for the job but no Node-RED node in the marketplace, you spend five hours figuring out how to set the library up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are short programs. Plain code is the right tool for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then one repo per deployment
&lt;/h2&gt;

&lt;p&gt;The next idea: each deployment gets its own codebase, built into one Docker container that holds all of that server's customisations.&lt;/p&gt;

&lt;p&gt;It works until the client disconnects the server from the internet. Then the integration still has to be written, so you open a shell in the container, edit the code with &lt;code&gt;vi&lt;/code&gt;, and hope nobody pulls the latest image before you have copied the same code back into the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually needed
&lt;/h2&gt;

&lt;p&gt;A VS Code-like editor in the browser, on the server, with the code directory mounted from outside so it survives a restart. If the code lives in one volume, and the volume is backed up, the integration doesn't need a repo at all.&lt;/p&gt;

&lt;p&gt;VS Code in the browser is heavy for this, because the language servers and compilers run on the server. I looked for a light editor and didn't find one, so I wrote codeshell.&lt;/p&gt;

&lt;h2&gt;
  
  
  What codeshell is
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Go, one ~10 MB static binary with the UI embedded. It serves plain files and PTYs. No language servers, no compilers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Svelte 5, CodeMirror 6 for the editor, xterm.js for terminals. Syntax modes load per file type.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/code&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The one required volume. Root of the file tree, working directory of every terminal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/code/start.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs on every container start, restart and recreate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size&lt;/td&gt;
&lt;td&gt;about 1,750 lines of Go and 3,700 of Svelte and TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Running one integration is one container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; myapp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;CODESHELL_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;change-me &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; myapp-code:/code &lt;span class="nt"&gt;-v&lt;/span&gt; myapp-state:/var/lib/codeshell &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/panth977/code-shell:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port 8080 is the editor. Port 3000 is whatever the integration listens on.&lt;/p&gt;

&lt;h2&gt;
  
  
  start.sh is the deploy
&lt;/h2&gt;

&lt;p&gt;Terminals are temporary on purpose. Anything you install or start from a terminal is gone after a restart. What has to come back goes in &lt;code&gt;start.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; node &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs npm&lt;span class="o"&gt;)&lt;/span&gt;
npm ci
&lt;span class="nb"&gt;exec &lt;/span&gt;node index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image is Debian slim, so &lt;code&gt;start.sh&lt;/code&gt; can &lt;code&gt;apt-get install&lt;/code&gt; whatever runtime the integration needs. On the first run it installs Node and the modules. On later runs &lt;code&gt;node_modules&lt;/code&gt; is already in the volume, and mounting &lt;code&gt;/root&lt;/code&gt; and &lt;code&gt;/opt&lt;/code&gt; as volumes too keeps package caches and runtimes, so restarts get faster.&lt;/p&gt;

&lt;p&gt;codeshell supervises the script. Its output shows as a &lt;code&gt;start.sh&lt;/code&gt; entry in the terminal panel, next to your own terminals, with Restart and Stop buttons, and its state is in the top bar: running, stopped, exited, waiting or missing. The restart policy is &lt;code&gt;on-failure&lt;/code&gt; by default, and restarts back off from 1 second, doubling, to a cap of 60 seconds. On a fresh volume it writes a commented &lt;code&gt;start.sh&lt;/code&gt; template with a Node and a Python example.&lt;/p&gt;

&lt;p&gt;Terminals keep their scrollback on the server, so a page reload or a dropped connection doesn't lose them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it sits
&lt;/h2&gt;

&lt;p&gt;The other half of our on-premise work is &lt;a href="https://panth.vardayinitech.in/blog/on-prem-manager-docker-deployments" rel="noopener noreferrer"&gt;On-Prem Manager&lt;/a&gt;, which deploys and updates the containers on each client server and snapshots their volumes. With codeshell, a client's integration is one container and one volume. On-Prem Manager can persist that volume's content to its cloud, where it is versioned and reviewed, so the integration needs no repo of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't try to be
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Single user. One login, an HMAC-signed session cookie, and repeated failed logins from one IP are locked out for longer each time.&lt;/li&gt;
&lt;li&gt;Anyone who signs in has a root shell in the container. Put it behind HTTPS and use a strong password. The password and the session secret are removed from the environment of terminals and &lt;code&gt;start.sh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No IntelliSense. It's an editor with syntax highlighting, quick open, split groups, find and replace, and a terminal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo is &lt;a href="https://github.com/Panth977/code-shell" rel="noopener noreferrer"&gt;github.com/Panth977/code-shell&lt;/a&gt;. Images are published for &lt;code&gt;linux/amd64&lt;/code&gt; and &lt;code&gt;linux/arm64&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Where do your per-client integrations live today, and what happens to them when the client's server goes offline?&lt;/p&gt;

</description>
      <category>docker</category>
      <category>go</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>90/10: what we took away from our AI coding agent, and why</title>
      <dc:creator>Panth Patel</dc:creator>
      <pubDate>Sat, 24 Oct 2026 13:01:16 +0000</pubDate>
      <link>https://dev.to/panthpatel/9010-what-we-took-away-from-our-ai-coding-agent-and-why-179g</link>
      <guid>https://dev.to/panthpatel/9010-what-we-took-away-from-our-ai-coding-agent-and-why-179g</guid>
      <description>&lt;p&gt;The first version of our agent pipeline let the coding agent run the git and deploy steps. It didn't fail often, but when it did, it failed in a different way each time, and I couldn't debug that. Today roughly 90% of the pipeline is plain scripted code, and the agent does one job.&lt;/p&gt;

&lt;p&gt;I'm Panth, and I lead the software team at Oizom, where Envizom is our environmental monitoring platform. Two days ago I wrote &lt;a href="https://panth.vardayinitech.in/blog/agent-pipeline-90-percent-scripts" rel="noopener noreferrer"&gt;the short version&lt;/a&gt;: the eight steps that run after a developer drags a ticket to Git Push. This is the long one. It follows one ticket through every column of the board and goes through each job we took off the agent, with the reason, then what it costs and what broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first version
&lt;/h2&gt;

&lt;p&gt;Before agents, our problems were ordinary ones. Envizom is several repos: hardware drivers, a simulator, a backend and a frontend. A new developer needed days to get a working local setup. Releases meant keeping track of which dev database, prod database, backend and frontend were pointed at each other, and as time went on I was the only person who knew every combination.&lt;/p&gt;

&lt;p&gt;Coding agents made my developers faster. With an agent, one developer can keep three tasks in flight. The mistakes then moved out of the code and into the branches around it. A change landed on the wrong branch, or a fix only worked on one machine. I wrote about that in &lt;a href="https://panth.vardayinitech.in/blog/one-stack-per-ticket" rel="noopener noreferrer"&gt;One stack per ticket&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the first version of the pipeline, the agent did more of the operational steps. It did the engineering, and it also ran git and the deploy. Compared with what we run now, it was slower and more expensive, and it was worse at the tasks that need to be boring.&lt;/p&gt;

&lt;p&gt;The failures taught me the most. A script that fails the same way twice is a bug you can fix: you read the log, find the line and change it. An agent that merges a little differently on each ticket gives you nothing to fix. Each failure is a new story, and the next run will tell another one.&lt;/p&gt;

&lt;p&gt;The split we ended up with follows one line. If a step is the same on every ticket, it is a script. The agent keeps the engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  One ticket, end to end
&lt;/h2&gt;

&lt;p&gt;The board has seven columns. One of them has the agent in it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Who acts&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Feedback&lt;/td&gt;
&lt;td&gt;developer&lt;/td&gt;
&lt;td&gt;writes what they want, in plain English. That is the whole spec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approved&lt;/td&gt;
&lt;td&gt;scripts&lt;/td&gt;
&lt;td&gt;dragging the ticket here is the handoff. The orchestrator provisions the ticket's stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Working&lt;/td&gt;
&lt;td&gt;agent&lt;/td&gt;
&lt;td&gt;does the engineering work inside that stack, and talks only through the ticket thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QAQC&lt;/td&gt;
&lt;td&gt;developer&lt;/td&gt;
&lt;td&gt;opens the preview the agent posted, tests it, replies in the thread or moves it on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git Push&lt;/td&gt;
&lt;td&gt;scripts&lt;/td&gt;
&lt;td&gt;commit, merge dev in, open and merge a pull request per repo, post the links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VM Update&lt;/td&gt;
&lt;td&gt;scripts&lt;/td&gt;
&lt;td&gt;wait for CI, roll the new images onto the dev VM, health checks, rollback if unhealthy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completed&lt;/td&gt;
&lt;td&gt;scripts&lt;/td&gt;
&lt;td&gt;the developer gets a notification, and the ticket's stack is torn down&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each developer has their own devbox on our dev VM: VS Code in the browser, with our orchestrator, orch, inside it. Every developer is paired with their own agent, and their orch only picks up tickets assigned to that agent. The board is shared. The machines and the agents are not.&lt;/p&gt;

&lt;p&gt;Here is a real ticket from the walkthrough video. I asked for a camera layout change on a device page. The agent built it in the ticket's stack and posted the preview. I replied in the thread: "also handle more than 4 cameras with a horizontal scroll, and set up 8 test cameras on this device". The ticket went back to the agent, which picked up with the context it already had, did both, and created the 8 test cameras in the ticket's own copy of the database. I tested the new preview and dragged the ticket to Git Push. After that, two notifications, and the change was live on dev.&lt;/p&gt;

&lt;p&gt;The person in that story touched the ticket three times: to write it, to reply to a preview, and to drag it. The agent had one column. Scripts ran the other four.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ten jobs scripts own
&lt;/h2&gt;

&lt;p&gt;This is the list no agent touches: watching the board, provisioning the stack, copying the database, issuing certificates, driving git, opening pull requests, waiting on CI, deploying, health checks and rollback. They fall into two groups, before the agent starts and after a person says yes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before the agent starts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Watching the board.&lt;/strong&gt; Orch watches for tickets dragged to Approved and assigned to its agent. Nothing here needs any thought. A ticket is either in the column or it isn't, and a model would add cost and nothing else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provisioning the stack.&lt;/strong&gt; Every approved ticket gets a fresh branch in every repo, named after the ticket, plus its own backend, drivers, simulator and frontend. This is the step everything else depends on, so it has to be the same every time. If the stack came out a little different per ticket, nothing built on top of it could be trusted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copying the database.&lt;/strong&gt; Each ticket gets a copy of the dev database, so it can change data freely. This is the safety decision in the whole design. An agent that runs a bad migration or deletes the wrong rows does it to the ticket's copy. Nobody else's work is in that box, so nobody else's work is touched. A step that sets up the safety net shouldn't be done by the thing the net is for.&lt;/p&gt;

&lt;p&gt;In Envizom V3, which we are rebuilding from scratch, the copy is cheap. Every store has a dev driver. Postgres, S3, the time-series database and Redis all become SQLite in dev, and MQTT becomes a service built into the Go backend. SQLite runs inside the Go process, so copying a database means copying a few files. That change made our sandboxes about 10x cheaper and faster than sandboxes with full databases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issuing certificates.&lt;/strong&gt; Every ticket has a private preview URL with an HTTPS certificate issued on demand. That URL is what the developer tests in QAQC, so it has to exist and work before the agent posts it.&lt;/p&gt;

&lt;h3&gt;
  
  
  After a person says yes
&lt;/h3&gt;

&lt;p&gt;When the developer drags a ticket to Git Push, no agent is involved from that moment on. Orch runs eight steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Git Push
  1  commit the work on the ticket branches
  2  merge the current dev branch into them
  3  open a pull request in each affected repo
  4  merge the pull requests
  5  post the PR and commit links into the ticket thread
VM Update
  6  wait for CI
  7  roll the new images onto the dev VM
  8  run health checks against the VM
       unhealthy -&amp;gt; roll back to the previous images, revert the merges,
                    tell the thread what happened
       healthy   -&amp;gt; Completed, notify the developer, tear the stack down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Driving git and opening pull requests.&lt;/strong&gt; One of our goals from the start was that using agents would not weaken the normal process. Every feature still gets its own branch, commits, pull request and review. That only holds if those steps happen the same way on every ticket. An agent deciding how to merge is not a feature. A script that always merges the same way is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Posting the links.&lt;/strong&gt; Step 5 looks small, but it is how the thread becomes the audit trail. The PR and commit links land next to the ask, the previews and the feedback, so one ticket holds the whole story of one change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waiting on CI.&lt;/strong&gt; Waiting is the clearest case of a job a model shouldn't hold. Nothing gets decided while it waits, and an agent session kept alive just to wait is still a session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploying, health checks and rollback.&lt;/strong&gt; These are the steps where sameness matters most. If the VM is unhealthy after the deploy, orch rolls it back to the previous images, reverts the merges and tells the thread, without anyone asking. A rollback that improvises is the last thing you want on the day something breaks. A scripted one runs the way it ran on every ticket before.&lt;/p&gt;

&lt;p&gt;All of it stops at the dev environment. Auto-merge, the VM update and the rollback apply to dev only. Production goes through a separate path, where people make the call on a slower cadence. That is on purpose. We don't want automation deciding what reaches production, and we don't want a fast feature list to become the goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taking away "I'm done"
&lt;/h2&gt;

&lt;p&gt;The same split shows up in &lt;a href="https://github.com/Panth977/ticket-tracker" rel="noopener noreferrer"&gt;ticket-tracker&lt;/a&gt;, the board I open-sourced, where a small orchestrator turns each ticket into a headless Claude Code session. (The per-ticket stack scripts are not in it. They are Envizom-specific code.) It takes a few more jobs off the agent, ones that are easy to miss because they don't look like jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deciding it is finished.&lt;/strong&gt; The agent never moves its own ticket. Its last message is structured output, enforced with &lt;code&gt;--json-schema&lt;/code&gt;:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OUTCOME_SCHEMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&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;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&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;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;review&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;waiting&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;blocked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;question_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&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;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&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;summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orchestrator reads it. &lt;code&gt;review&lt;/code&gt; moves the ticket to Review. &lt;code&gt;waiting&lt;/code&gt; means a person is needed, so nothing runs until someone answers. &lt;code&gt;blocked&lt;/code&gt; usually means the turn ran out of room, so the orchestrator resumes it by itself, up to 3 times, and then says on the ticket that it has stopped. The agent reports. The script decides what that means for the board.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saying it is alive.&lt;/strong&gt; The heartbeat comes from the orchestrator, not the agent: one beat every 60 seconds while the agent works. A crashed process can't report that it crashed, so health is worked out from the age of the last beat. The agent's part is its task list, which the orchestrator reads for the status message. More in &lt;a href="https://panth.vardayinitech.in/blog/agent-heartbeat-derived-health" rel="noopener noreferrer"&gt;Agent health from a 60-second heartbeat&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Working out what a turn cost.&lt;/strong&gt; When a run ends, the orchestrator reads Claude Code's result line and posts the turn's receipt on the ticket. The app's comment box never posts one. Each receipt goes out with an idempotency key made from the ticket and the run number, so a retried request doesn't count a turn twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waiting for an answer.&lt;/strong&gt; When the agent needs a decision, it asks in the ticket and ends its turn. It is told not to poll. My answer starts the same session again with &lt;code&gt;--resume&lt;/code&gt;. &lt;a href="https://panth.vardayinitech.in/blog/questions-end-the-turn" rel="noopener noreferrer"&gt;Headless Claude Code asks, exits, then resumes on the answer&lt;/a&gt; has the details.&lt;/p&gt;

&lt;p&gt;Any of these could be left to the agent. Each one is the same on every ticket, so none of them is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent still does
&lt;/h2&gt;

&lt;p&gt;The engineering. It reads the ticket, works in the ticket's own repos and services, changes the code, and creates the test data it needs in the ticket's database copy, as in the 8 test cameras. When the developer replies with feedback, it picks up with the same context it had before, without re-reading the repo to find out where it was.&lt;/p&gt;

&lt;p&gt;It also asks. A real decision that belongs to the owner, such as scope, design, or anything destructive or outward-facing, becomes a question in the ticket rather than a guess.&lt;/p&gt;

&lt;p&gt;That is the 10%. It is the only part of the pipeline where the right answer changes from ticket to ticket, so it is the only part where a model is worth what it costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stays with people
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Writing the ticket.&lt;/strong&gt; The thread is the only way to talk to the agent, so every prompt is stored with the work it produced. I read how each person on the team asks for things and coach them on it. That has done more than any prompting guide. See &lt;a href="https://panth.vardayinitech.in/blog/prompts-stored-in-the-ticket" rel="noopener noreferrer"&gt;Our coding agent prompts live in the ticket&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;QA.&lt;/strong&gt; The agent posts a preview and moves the ticket to QAQC, and that is as far as it goes. The developer who asked tests the preview, because they are the only one who knows what they meant. An agent grading its own work would remove the one review that matters most. Every step after QA is built to be boring and repeatable, and none of them asks whether the change was the right change. QA is where that question gets asked, so it is the one place we didn't automate. More in &lt;a href="https://panth.vardayinitech.in/blog/qa-stays-human" rel="noopener noreferrer"&gt;1 human check, 8 scripted steps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The drag to Git Push.&lt;/strong&gt; Shipping to dev is a person's action in the thread. Shipping to production is a separate, slower decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Number&lt;/th&gt;
&lt;th&gt;Source of the number&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One agent turn in the walkthrough video&lt;/td&gt;
&lt;td&gt;$4.77&lt;/td&gt;
&lt;td&gt;the receipt under the agent's reply: "turn $4.77 · total $4.77"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The dev VM with every devbox and ticket stack&lt;/td&gt;
&lt;td&gt;about ₹4–6k a month&lt;/td&gt;
&lt;td&gt;rented, one machine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tickets it runs in parallel&lt;/td&gt;
&lt;td&gt;6, comfortably&lt;/td&gt;
&lt;td&gt;same VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sandbox cost after SQLite dev drivers&lt;/td&gt;
&lt;td&gt;about 10x cheaper and faster&lt;/td&gt;
&lt;td&gt;V3 rebuild&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A scripted step&lt;/td&gt;
&lt;td&gt;no turn receipt&lt;/td&gt;
&lt;td&gt;it is not a model call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the plain reason scripts are cheaper. Every agent turn posts its cost into the ticket thread, right under the answer. A scripted merge, deploy or health check posts no receipt, because it doesn't call a model. An operational step done by the agent is done inside a billed turn.&lt;/p&gt;

&lt;p&gt;The receipt is a design choice too. The person who asked sees what the ask cost, where they asked it, and it changes how they write the next one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke on the scripted side
&lt;/h2&gt;

&lt;p&gt;Scripts break as well. The difference is how they break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost number was wrong.&lt;/strong&gt; On ticket-tracker, one ticket reached $402 over nine agent runs and nothing in the app said so. The orchestrator already wrote the cost of every run to a local log, but nothing showed it where the work was. Putting the number on the ticket had a trap: Claude Code's &lt;code&gt;total_cost_usd&lt;/code&gt; is cumulative across &lt;code&gt;--resume&lt;/code&gt;. Run 8 made one API call with zero output tokens and reported $392.83, the same as run 7. The fix is a few lines in the orchestrator. It keeps the last total per session and posts the difference. The full fix is in &lt;a href="https://panth.vardayinitech.in/blog/cost-per-turn-headless-claude-code" rel="noopener noreferrer"&gt;$402 on one ticket&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polling was most of the bill.&lt;/strong&gt; Two orchestrator loops asked the tracker's API for the full ticket and event lists every 30 seconds or so. Over three days that was 8,822 + 8,815 calls, 88% of every request the API served. The fix was again in the script. Agents now hold one stream on a small revision node and fetch only the delta when it moves. That one is in &lt;a href="https://panth.vardayinitech.in/blog/wake-on-a-revision-not-polling" rel="noopener noreferrer"&gt;Polling was 88% of my API calls&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The heartbeat was on the bill twice.&lt;/strong&gt; The first version kept each beat stream as a Firestore document: 1,440 writes per agent per day, plus a sweep that ran every minute to notice silence, 4,261 runs in three days. Beats moved to the Realtime Database, about 100 bytes each, and silence is now worked out at read time from the age of the last beat.&lt;/p&gt;

&lt;p&gt;None of these needed an agent to diagnose. Each showed up in a log or a bill, and each was fixed in one place in the code. That is the trade I wanted from the split. The agent's mistakes stay inside a ticket's stack, where a bad change hit a copy. The scripts' mistakes show up in a log, the same way twice, so they can be fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the boring 90% makes the 10% useful
&lt;/h2&gt;

&lt;p&gt;Most of what makes the pipeline worth using comes from the scripted part. Per-ticket isolation is what lets the agent work freely, and lets a ticket sit in QAQC for a week while three others ship. The thread is the audit trail because scripts post into it at every step: the preview, the cost of each turn, the PR and commit links, and the rollback if there is one. Stored prompts are reviewable because the thread is the only channel. Human QA and dev-only scope are the two places we chose not to automate, and both are easier to hold when everything around them is predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order I'd build it in
&lt;/h2&gt;

&lt;p&gt;If you're setting up something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Isolation first. If every task can't run in its own branch, with its own services and its own data, nothing after it is safe.&lt;/li&gt;
&lt;li&gt;A handoff surface the team already uses. For us that was the ticket.&lt;/li&gt;
&lt;li&gt;Script every step that is the same on every ticket: provisioning, git, CI, deploy, health, rollback, and the receipts.&lt;/li&gt;
&lt;li&gt;Only then, the agent inside the box, with the smallest job you can give it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://panth.vardayinitech.in/blog/ticket-is-the-interface" rel="noopener noreferrer"&gt;The ticket is the interface&lt;/a&gt; has the walkthrough video and the first write-up of the pipeline.&lt;/p&gt;

&lt;p&gt;If your team runs coding agents, which step is the agent doing today that a script would do the same way every time?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I lead the software team at &lt;a href="https://oizom.com" rel="noopener noreferrer"&gt;Oizom&lt;/a&gt;, where Envizom turns data from air-quality and environmental monitors in 80+ countries into something cities and industries can act on. I dropped out before the AI wave, because I figured the engineering was in solving problems in the real world, and my college made me choose between attendance and real-world applications. Find me on &lt;a href="https://www.linkedin.com/in/panth-patel-447a88240/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>automation</category>
      <category>cicd</category>
    </item>
    <item>
      <title>On-Prem Manager: one engineer per two on-prem Docker sites didn't scale</title>
      <dc:creator>Panth Patel</dc:creator>
      <pubDate>Fri, 23 Oct 2026 13:01:04 +0000</pubDate>
      <link>https://dev.to/panthpatel/on-prem-manager-one-engineer-per-two-on-prem-docker-sites-didnt-scale-4fo6</link>
      <guid>https://dev.to/panthpatel/on-prem-manager-one-engineer-per-two-on-prem-docker-sites-didnt-scale-4fo6</guid>
      <description>&lt;p&gt;About five on-premise deployments a year, and one dedicated engineer for every two of them. That was the shape of our setup when we realised on-premise was going to be most of our business.&lt;/p&gt;

&lt;p&gt;I'm Panth, and I lead the software team at Oizom, where we build Envizom, an air quality monitoring platform. This post is about the deployment problem that came with selling it, and &lt;a href="https://github.com/Panth977/on-premise-manager" rel="noopener noreferrer"&gt;On-Prem Manager&lt;/a&gt; (OPM), the tool I built and open-sourced under Apache-2.0 to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why on-premise
&lt;/h2&gt;

&lt;p&gt;Many organisations don't want to hand their data to a cloud. Governments most of all, and air quality is first a government responsibility. So when a government picks software like Envizom, it also needs that software on its own servers, for its own security reasons.&lt;/p&gt;

&lt;p&gt;The more we sold, the clearer it got: local deployment is the one real selling point for an air quality platform, and per-client customisation is what wins the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a deployment actually looks like
&lt;/h2&gt;

&lt;p&gt;The ideal case: the client gives you exactly what you asked for, and if something breaks on their side, a firewall rule or an IP config, they own it.&lt;/p&gt;

&lt;p&gt;That is not what happens. One client wants a DC/DR setup. Another wants high availability. Another wants a single server. Another wants DC/DR eventually, but for now on one server, with a second one later and two more after that.&lt;/p&gt;

&lt;p&gt;Then there are the integrations: pull data from one of their systems, or expose an API in a schema they hand us for a public display. None of that belongs in our production code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we had
&lt;/h2&gt;

&lt;p&gt;An end-to-end setup on Docker Compose, with Portainer as the GUI.&lt;/p&gt;

&lt;p&gt;After enough deployments it was clear Compose wasn't enough. Every client is different, so every install meant a lot of configuration and setup afterwards. A simple issue took hours to close, because debugging a remote server through that stack is slow.&lt;/p&gt;

&lt;p&gt;That setup is fine at five deployments a year with one engineer for every two. It is the bottleneck the moment you want more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the idea came from
&lt;/h2&gt;

&lt;p&gt;Oizom started as an IoT company. It ships devices with software inside, and every device sits at a remote location where nobody can do anything by hand. For that, Oizom uses a device manager, Aikaan.&lt;/p&gt;

&lt;p&gt;So: what if our deployment team had the same thing for on-premise servers?&lt;/p&gt;

&lt;p&gt;What the team needs to run a deployment is a short list. Automated volume backups. A way to make a new version live. Environment and config per site. Variables, with separate secrets for each deployment. A GUI to see the containers. I wrote down every problem we hit in operations and built a tool around the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OPM is
&lt;/h2&gt;

&lt;p&gt;Two parts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud console&lt;/td&gt;
&lt;td&gt;The catalog of images, deployment templates, variables, secrets and files. You commit a change once and review the diff per site. Self-host it on a VM (SQLite and local files) or run it on Firebase, same frontend build.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;td&gt;One Go binary per server, with its own GUI built in. It applies commits to that server's Docker, runs an embedded Caddy as the edge proxy, snapshots volumes, and rolls back.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The README puts it as: like Portainer, plus a git-style commit history for every server's config.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install is one line
&lt;/h3&gt;

&lt;p&gt;The cloud's Setup page gives a command per server:&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;-fsSL&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer opi_…"&lt;/span&gt; &amp;lt;apiBase&amp;gt;/v1/install/script | &lt;span class="nb"&gt;sudo &lt;/span&gt;sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs preflight checks, uses the Docker that is already there (20.10 or newer) or asks before installing a pinned version, downloads the agent and checks its sha256 and Ed25519 signature, writes the config, sets up a systemd service, and waits up to 60 seconds for a health check, rolling itself back if that fails. Everything it installs is pinned when the script is generated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Images without a registry
&lt;/h3&gt;

&lt;p&gt;You register images in the cloud's catalog, and the cloud keeps its own copy of their layers. The agent loads images from those blobs with &lt;code&gt;docker load&lt;/code&gt; and never connects to a Docker registry, so it never sees a registry password. Sites in the same deployment copy layers from each other first, and only one site with internet fetches from the cloud.&lt;/p&gt;

&lt;h3&gt;
  
  
  Commits, not proposals
&lt;/h3&gt;

&lt;p&gt;Templates, deployments and each site are branches of commits. The latest commit on a deployment's branch is what its sites should run. You can compare any two branches and cherry-pick between them.&lt;/p&gt;

&lt;p&gt;Nothing changes on a site until a person on that site merges it, in the agent GUI. A merge runs one fixed sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LOCK -&amp;gt; SNAPSHOT -&amp;gt; APPLY -&amp;gt; VERIFY -&amp;gt; COMMIT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VERIFY waits for each container to pass its health check, or to run 10 seconds without a restart, within 300 seconds by default. Any failure rolls back, and a failed merge leaves no commit. The failed container is kept for 24 hours under a &lt;code&gt;-failed-&amp;lt;time&amp;gt;&lt;/code&gt; name, with its last 200 log lines, so the post-mortem doesn't depend on someone grabbing logs in time.&lt;/p&gt;

&lt;p&gt;The dangerous act isn't the merge, it's restoring data. So rollbacks and volume restores get a 60-second countdown and a typed &lt;code&gt;ACCEPT&lt;/code&gt;. The spec's reasoning: "the commit message + plan review are the deliberate act… restoring data is still the dangerous act".&lt;/p&gt;

&lt;h3&gt;
  
  
  Backups by default
&lt;/h3&gt;

&lt;p&gt;Before every update and rollback, the agent snapshots config, image references and the volumes marked for backup, as &lt;code&gt;.tar.zst&lt;/code&gt; archives. It keeps the last 10. A scheduled snapshot runs daily at 02:00 site time and keeps the last 7. A snapshot refuses to start if free disk is under 1.2 times its estimate. Snapshots download as signed &lt;code&gt;.opmsnap&lt;/code&gt; files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker stays the source of truth
&lt;/h3&gt;

&lt;p&gt;People still change things in Docker directly. The agent scans every 60 seconds and on Docker events. A hand edit to an image, env, port, mount or file shows up as uncommitted drift, and merges are refused until it is committed or discarded. A site can also auto-commit drift once it has been stable for 30 seconds, authored as "changed in Docker".&lt;/p&gt;

&lt;h3&gt;
  
  
  Standalone first
&lt;/h3&gt;

&lt;p&gt;Every agent GUI can do everything locally with no cloud at all: create containers, change images, edit files, variables and secrets. The cloud is how you run many sites, not something a site needs to keep running.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the client cuts the internet
&lt;/h2&gt;

&lt;p&gt;Some clients take the server off the internet for security. OPM has three routes to the cloud, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The server's own internet.&lt;/li&gt;
&lt;li&gt;A peer site in the same deployment that has internet, relaying.&lt;/li&gt;
&lt;li&gt;The browser of the person using the agent GUI. The GUI tab long-polls the agent and runs the cloud requests from the browser, so an engineer's laptop on the client network becomes the tunnel.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent only ever talks HTTPS on 443 to one API domain, which is one line in a client's firewall request.&lt;/p&gt;

&lt;p&gt;And for a client that allows neither the agent nor the cloud, the cloud can mirror every branch to a GitHub repository. A machine checks out its site's branch, fills in &lt;code&gt;site.env&lt;/code&gt; and runs &lt;code&gt;./deploy.sh&lt;/code&gt;: plain &lt;code&gt;docker compose&lt;/code&gt;, Caddy as the router, Portainer as the GUI, no OPM code on the box. A real-Docker test compares that output with the agent's and reports 0 differences. You lose drift detection, snapshots and rollback, which is why it's the last resort.&lt;/p&gt;

&lt;h2&gt;
  
  
  The size of it
&lt;/h2&gt;

&lt;p&gt;The full specification, &lt;code&gt;SPEC-final.md&lt;/code&gt;, is 894 KB, and its decision log has 116 numbered entries, each with the reason. The code is about 97,000 lines of Go, 96,000 of TypeScript and 60,000 of Svelte, with 105 Go test files and 157 TypeScript test files. There is an end-to-end harness that runs real agents against real Docker.&lt;/p&gt;

&lt;p&gt;Integrations got their own tool too. They run in &lt;a href="https://github.com/Panth977/code-shell" rel="noopener noreferrer"&gt;codeshell&lt;/a&gt;, a small browser editor in a container whose code is one volume. OPM's "Persist to cloud" versions a volume's content in the cloud, the docs name "client integration code" as the use, so that code is reviewed and kept there instead of in a git repository. codeshell gets its own post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd ask you
&lt;/h2&gt;

&lt;p&gt;If you ship software that customers run on their own servers: what does one deployment cost you in engineer-hours, and which part of it did you automate first?&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>go</category>
      <category>opensource</category>
    </item>
    <item>
      <title>90% of our AI coding agent pipeline is plain scripted code</title>
      <dc:creator>Panth Patel</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:06:35 +0000</pubDate>
      <link>https://dev.to/panthpatel/90-of-our-ai-coding-agent-pipeline-is-plain-scripted-code-2mn4</link>
      <guid>https://dev.to/panthpatel/90-of-our-ai-coding-agent-pipeline-is-plain-scripted-code-2mn4</guid>
      <description>&lt;p&gt;Roughly 90% of the pipeline that ships our code is plain scripted code. The AI coding agent does one thing: the engineering work inside a single ticket's workspace, in response to what a person wrote in that ticket.&lt;/p&gt;

&lt;p&gt;We didn't start there. The first version gave the agent more of the operational steps, and this post is about why we took them back.&lt;/p&gt;

&lt;p&gt;I lead the software team at Oizom, where Envizom is our environmental monitoring platform. The pipeline below is our dev pipeline for Envizom.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;

&lt;p&gt;A developer writes what they want in a ticket, in plain English, and drags it to &lt;strong&gt;Approved&lt;/strong&gt;. Our orchestrator picks it up and gives the ticket its own stack: a branch in every repo named after the ticket, its own backend, drivers, simulator and frontend, a copy of the dev database, and a private preview URL with an HTTPS certificate issued on demand.&lt;/p&gt;

&lt;p&gt;The agent works inside that stack and talks to the developer only through the ticket thread. When it's done it posts the preview link and moves the ticket to &lt;strong&gt;QAQC&lt;/strong&gt;. The developer tests the preview, replies in the thread, and either sends it back or drags it on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Approved ──► [scripts] provision stack ──► [agent] engineering work ──► QAQC
                                                    ▲                    │
                                                    └── feedback ────────┤
                                                                         ▼
Completed ◄── [scripts] commit, merge, PR, CI, deploy, health ◄── Git Push
     │                         │
     │                         └── unhealthy: roll back images, revert merges
     ▼
 stack torn down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One box in that diagram has the agent in it. Everything else is a script.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the scripts do
&lt;/h2&gt;

&lt;p&gt;Here is the list of jobs no agent touches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;watching the board&lt;/li&gt;
&lt;li&gt;provisioning the per-ticket stack&lt;/li&gt;
&lt;li&gt;copying the database&lt;/li&gt;
&lt;li&gt;issuing certificates&lt;/li&gt;
&lt;li&gt;driving git&lt;/li&gt;
&lt;li&gt;opening pull requests&lt;/li&gt;
&lt;li&gt;waiting on CI&lt;/li&gt;
&lt;li&gt;deploying&lt;/li&gt;
&lt;li&gt;health checks&lt;/li&gt;
&lt;li&gt;rollback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The clearest example is what happens when a developer drags a ticket to &lt;strong&gt;Git Push&lt;/strong&gt;. From that moment there is no agent involved. The orchestrator runs eight steps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Done by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Commit the work on the ticket branches&lt;/td&gt;
&lt;td&gt;script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Merge the current dev branch into them&lt;/td&gt;
&lt;td&gt;script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Open a pull request in each affected repo&lt;/td&gt;
&lt;td&gt;script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Merge the pull requests&lt;/td&gt;
&lt;td&gt;script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Post the PR and commit links back into the ticket thread&lt;/td&gt;
&lt;td&gt;script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Wait for CI&lt;/td&gt;
&lt;td&gt;script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Roll the new images onto the dev VM&lt;/td&gt;
&lt;td&gt;script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Run health checks against the VM&lt;/td&gt;
&lt;td&gt;script&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the VM isn't healthy afterwards, the orchestrator rolls it back to the previous images and reverts the merges, on its own, and then tells the thread what happened. If it is healthy, the ticket moves to &lt;strong&gt;Completed&lt;/strong&gt; and the developer gets a notification. The ticket's stack is torn down after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we took the steps back from the agent
&lt;/h2&gt;

&lt;p&gt;We tried it the other way first, with the agent doing more of the operational steps. It was slower, it was more expensive, and it was worse at exactly the tasks that need to be boring.&lt;/p&gt;

&lt;p&gt;That last one is the reason that matters. Merging, deploying and rolling back should happen the same way on every ticket, and that sameness is the whole value. An agent deciding how to merge is not a feature. A script that always merges the same way is.&lt;/p&gt;

&lt;p&gt;The scripted part is deterministic and tested. When it breaks, I read a log and fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the agent earns its place
&lt;/h2&gt;

&lt;p&gt;The agent's job is the engineering work itself. In the walkthrough video I asked for a camera layout change on a device page. The agent shipped it and posted the preview. I replied in the thread asking it to also handle more than 4 cameras with a horizontal scroll, and to set up 8 test cameras on that device. It did both, including creating the test data in the ticket's own database copy, and it picked up with the same context it had before.&lt;/p&gt;

&lt;p&gt;Isolation is what lets it do that safely. Because every ticket has its own stack, an agent that does something silly to the database did it to a copy. A developer can have several tickets in flight at once, and one ticket can sit in QAQC for a week while three others ship.&lt;/p&gt;

&lt;p&gt;Two more lines we drew on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;QA is human.&lt;/strong&gt; The developer tests the preview and decides whether the ticket moves forward. An agent grading its own work would remove the one review that matters most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This pipeline stops at dev.&lt;/strong&gt; Auto-merge, rollback and VM updates all apply to the dev environment only. Production goes through a separate path where people make the call, on a slower cadence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The order I'd build it in
&lt;/h2&gt;

&lt;p&gt;If you're setting up something similar, this is the order we ended up with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Isolation first. If every task can't run in its own branch, its own services and its own data, nothing after it is safe.&lt;/li&gt;
&lt;li&gt;A handoff surface the team already uses. For us that was the ticket.&lt;/li&gt;
&lt;li&gt;Only then, the agent inside the box.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And keep the box scripted. The agent should be the smallest part of the system.&lt;/p&gt;

&lt;p&gt;The longer write-up, with the video walkthrough, is &lt;a href="https://panth.vardayinitech.in/blog/ticket-is-the-interface" rel="noopener noreferrer"&gt;The ticket is the interface&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If your team runs coding agents, which step did you hand to the agent that a script should own?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>automation</category>
      <category>cicd</category>
    </item>
    <item>
      <title>0 reads to switch views: Firestore board filters in the browser</title>
      <dc:creator>Panth Patel</dc:creator>
      <pubDate>Thu, 22 Oct 2026 19:57:41 +0000</pubDate>
      <link>https://dev.to/panthpatel/0-reads-to-switch-views-firestore-board-filters-in-the-browser-187a</link>
      <guid>https://dev.to/panthpatel/0-reads-to-switch-views-firestore-board-filters-in-the-browser-187a</guid>
      <description>&lt;p&gt;Switching between saved views on a board in my tracker costs zero Firestore reads. The board's active tickets are loaded once and kept live, and every filter, sort, group-by and swimlane is a pure function run over them in the browser.&lt;/p&gt;

&lt;p&gt;The constraint that forced it: Firestore cannot OR across fields, and every combination of filters you want to query needs its own composite index. A tracker with user-defined views over custom fields would need thousands of them.&lt;/p&gt;

&lt;p&gt;I'm Panth, and I lead the software team at Oizom. This is from &lt;a href="https://github.com/Panth977/ticket-tracker" rel="noopener noreferrer"&gt;ticket-tracker&lt;/a&gt;, my open-source board. Its decisions page lists the choices that are expensive to reverse, each with the alternative that was turned down. This is D5.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chosen&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One live query per board over its active tickets. Filter, sort, group-by and swimlanes are pure functions in &lt;code&gt;packages/shared&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rejected&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One Firestore query per view.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Why&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Firestore cannot OR across fields, cannot filter on range and inequality over two fields, and needs a composite index for every combination. User-defined views with custom fields would need thousands of indexes. In the browser, switching views is instant and costs no reads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ceiling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;About 5,000 active tickets per board, or about 5 MB.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A saved view is a few lines of data. A filter is a tree of &lt;code&gt;and&lt;/code&gt; / &lt;code&gt;or&lt;/code&gt; groups over leaves:&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;type&lt;/span&gt; &lt;span class="nx"&gt;FilterGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;and&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;or&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FilterNode&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FilterLeaf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&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;cmp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cmp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;FilterNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FilterGroup&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;FilterLeaf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A leaf can compare any built-in field (stage, priority, assignee, tag, due date, state, creator, text, linked) or any custom field, with twelve comparisons from &lt;code&gt;is&lt;/code&gt; to &lt;code&gt;between&lt;/code&gt;. "Mine and high priority, or anything of Ann's" is this, straight from the tests:&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;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FilterNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;or&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;and&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assignee&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cmp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;is&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;me&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;priority&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cmp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;is&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assignee&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cmp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;is&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ann&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a Firestore query, that OR spans two fields, and the tree can nest as deep as people like. As a function over an array, it's a recursive &lt;code&gt;evalNode&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One function draws every view
&lt;/h2&gt;

&lt;p&gt;The whole engine is one call:&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="nf"&gt;applyView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tickets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does four things, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep the ticket states the view asks for. The default is active only, so archived tickets leave every default view.&lt;/li&gt;
&lt;li&gt;Run the filter tree.&lt;/li&gt;
&lt;li&gt;Sort on several keys, ties broken by rank and then id, so every client gets the same order.&lt;/li&gt;
&lt;li&gt;Group, then group again inside each group for swimlanes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Kanban, table, calendar and timeline all draw from what it returns. A ticket with two assignees or three tags appears in each of their groups. Swimlanes are computed over all visible tickets, so every column has the same rows.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;me&lt;/code&gt;, &lt;code&gt;today&lt;/code&gt;, &lt;code&gt;thisWeek&lt;/code&gt; and &lt;code&gt;overdue&lt;/code&gt; are tokens in the saved view, resolved when the function runs, against the viewer's id and time zone. A shared view called "Mine, due this week" means the same thing to everyone who opens it, each in their own week.&lt;/p&gt;

&lt;p&gt;Because the function is pure, it's tested as one: the test file is 453 lines against 665 lines of engine, with a fixed clock (Tuesday 2026-09-22 in Kolkata) and a board with six custom field types.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one query, and what it costs
&lt;/h2&gt;

&lt;p&gt;The query that feeds the engine is about as simple as Firestore gets: the board's tickets where &lt;code&gt;state == 'active'&lt;/code&gt;, one listener per board. It needs no composite index.&lt;/p&gt;

&lt;p&gt;Loading the whole board on every open would pay one read per card. So the board store is incremental. The first open runs the full query. After that, the device starts from Firestore's own IndexedDB cache of the last snapshot and asks only for tickets whose &lt;code&gt;updatedAt&lt;/code&gt; is newer than the newest one it has seen. An unchanged board answers that with no documents, which Firestore bills as one read, whatever the board's size.&lt;/p&gt;

&lt;p&gt;The delta query has no &lt;code&gt;state&lt;/code&gt; filter, on purpose. A ticket that leaves the active set has to arrive so it can be dropped from the board, and a query for active tickets would never send it. The browser decides what to keep. One inequality on one field also means the delta needs no composite index.&lt;/p&gt;

&lt;p&gt;A hard delete doesn't show up in any &lt;code&gt;updatedAt &amp;gt;&lt;/code&gt; query, because the document is gone. So the watermark is trusted for 6 hours from when it was written. After that, the next open pays for one full query and agrees with the server again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stops
&lt;/h2&gt;

&lt;p&gt;The decision writes down its own ceiling: about 5,000 active tickets, or about 5 MB, per board. Past that, the plan is to flag the board as large and run its views through Typesense, which can filter on every field, with paging. Archived tickets are never loaded by default, so the number that matters is open work, not history.&lt;/p&gt;

&lt;p&gt;For a personal tool, where a board is the unit people share, 5,000 open tickets is far more than a board should hold. The trade is the right one for this product. It wouldn't be for a tracker that hosts a whole company on one board.&lt;/p&gt;

&lt;p&gt;What I'd take from this for any Firestore app with user-built filters: if the data a screen needs fits on the device, query it once, keep it live, and do the rest in code you can unit test. Indexes are for the data that doesn't fit.&lt;/p&gt;

&lt;p&gt;Where do your filters run: in the database, or over data you've already loaded?&lt;/p&gt;

</description>
      <category>firestore</category>
      <category>sveltekit</category>
      <category>typescript</category>
      <category>kanban</category>
    </item>
    <item>
      <title>MCP on OAuth 2.1: one URL and a consent click, no API key</title>
      <dc:creator>Panth Patel</dc:creator>
      <pubDate>Tue, 20 Oct 2026 23:52:57 +0000</pubDate>
      <link>https://dev.to/panthpatel/mcp-on-oauth-21-one-url-and-a-consent-click-no-api-key-3af8</link>
      <guid>https://dev.to/panthpatel/mcp-on-oauth-21-one-url-and-a-consent-click-no-api-key-3af8</guid>
      <description>&lt;p&gt;Adding my tracker to Claude takes one URL and one click on a consent screen. Nobody makes a key, copies it, or pastes it anywhere.&lt;/p&gt;

&lt;p&gt;The constraint: an MCP client that a person uses every day should not hold a long-lived secret the person had to handle by hand, and the person should be able to see and cut off what it can reach.&lt;/p&gt;

&lt;p&gt;I'm Panth, and I lead the software team at Oizom. This is from &lt;a href="https://github.com/Panth977/ticket-tracker" rel="noopener noreferrer"&gt;ticket-tracker&lt;/a&gt;, my open-source board. Its decisions page lists the choices that are expensive to reverse, each with the alternative that was turned down. This is D9.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chosen&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OAuth 2.1 with PKCE, dynamic client registration, and protected-resource metadata, as the MCP authorization spec expects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rejected&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pasting an API key into the MCP client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Why&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;It is the only way a person adds the server with one URL and a consent click, in Claude and other clients. The grant is per person, can be revoked in Account › Connected apps, and every change it makes is attributed &lt;code&gt;via MCP&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;API keys did not go away. They still work on &lt;code&gt;/mcp&lt;/code&gt; for headless setups, where nobody is there to click Allow. Claude Code takes a token in a header, and so do the orchestrators that run my agents. OAuth is for the case where a person is sitting in front of the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the client does, with nobody pasting anything
&lt;/h2&gt;

&lt;p&gt;The whole flow is discovery. The client starts knowing one URL, and every other address comes from a document the server publishes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. POST /mcp, no token
   ← 401  WWW-Authenticate: Bearer resource_metadata=".../.well-known/oauth-protected-resource/mcp"
2. GET  /.well-known/oauth-protected-resource/mcp        (RFC 9728)
   ← { authorization_servers: [base], scopes_supported: [...] }
3. GET  /.well-known/oauth-authorization-server          (RFC 8414)
   ← authorize, token, register and revoke endpoints; PKCE S256 only
4. POST /oauth/register                                  (RFC 7591)
   ← client_id
5. GET  /oauth/authorize?...&amp;amp;code_challenge=...  → our consent screen
6. person picks boards, presses Allow  → redirect with ?code=
7. POST /oauth/token  code + code_verifier  ← access + refresh tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 1 carries the design. The 401 is not "your token is bad". It is "go and sign in, and here is where to start". A client that has never heard of this server can follow the header all the way to a working token.&lt;/p&gt;

&lt;p&gt;Some clients probe OIDC discovery first, so &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt; answers with the same document.&lt;/p&gt;

&lt;h2&gt;
  
  
  The consent screen is where scope gets decided
&lt;/h2&gt;

&lt;p&gt;The screen names the app asking, lists the permissions it wants in plain words ("Read tickets", "Post, edit, pin and react in threads"), and asks which boards it may act on: every board, or the ones you tick. Nothing is granted until you press Allow.&lt;/p&gt;

&lt;p&gt;The server checks the ticked boards against the boards the person can actually read. A board you are not on is an error, not a silent drop.&lt;/p&gt;

&lt;p&gt;What the grant gives back shapes the MCP server itself. A tool the grant's scopes do not allow is not registered at all, so it is missing from &lt;code&gt;tools/list&lt;/code&gt;, and every handler checks again anyway. The model never sees a tool it would only get a 403 from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token lifetimes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token&lt;/th&gt;
&lt;th&gt;Lives&lt;/th&gt;
&lt;th&gt;Rules&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Authorization code&lt;/td&gt;
&lt;td&gt;60 s&lt;/td&gt;
&lt;td&gt;Single use, bound to the client, the redirect URI and the PKCE S256 challenge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access token&lt;/td&gt;
&lt;td&gt;1 h&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh token&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;Rotated on every use&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tokens are stored only as their SHA-256. The grant is a document under the person (&lt;code&gt;users/{uid}/oauthGrants/{grantId}&lt;/code&gt;), and it is the revocation switch: revoking deletes it and every token issued under it. Revoking in Connected apps stops the app on its next call, including sessions already open.&lt;/p&gt;

&lt;p&gt;A refresh may narrow scopes, never widen them. Asking for a scope the grant does not hold fails with &lt;code&gt;invalid_scope&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that catches a stolen refresh token
&lt;/h2&gt;

&lt;p&gt;Rotation alone does not help if nobody notices the old token coming back. So a used refresh token is kept, marked with when it was used, until it expires. If it is presented again, that is two parties holding the same token: the real client and someone else. The server revokes the whole grant.&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;// A client that refreshes twice at once, or retries after losing the response,&lt;/span&gt;
&lt;span class="c1"&gt;// presents the same token seconds apart: that is not theft.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usedAt&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;REFRESH_RETRY_GRACE_MS&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;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usedAt&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// THEFT DETECTION: an already-rotated refresh token came back.&lt;/span&gt;
  &lt;span class="nx"&gt;reused&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grantId&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The grace window is 60 seconds. Without it, a client that fires two refreshes in parallel, or loses the response and retries, would lock its own user out. With it, a retry gets a fresh pair and a replay an hour later kills the grant with "Refresh token reuse detected — the app must be connected again".&lt;/p&gt;

&lt;h2&gt;
  
  
  The small checks that keep it from becoming an open door
&lt;/h2&gt;

&lt;p&gt;Dynamic registration means anyone can register a client, so the edges matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Registration is rate-limited&lt;/strong&gt; per IP address: 20 a minute, 500 a day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect URIs&lt;/strong&gt; must be &lt;code&gt;https&lt;/code&gt;, or &lt;code&gt;http&lt;/code&gt; on &lt;code&gt;localhost&lt;/code&gt;, &lt;code&gt;127.0.0.1&lt;/code&gt; or &lt;code&gt;[::1]&lt;/code&gt; only, or a private-use scheme such as &lt;code&gt;cursor://&lt;/code&gt; or &lt;code&gt;vscode://&lt;/code&gt;. &lt;code&gt;javascript:&lt;/code&gt;, &lt;code&gt;data:&lt;/code&gt;, &lt;code&gt;file:&lt;/code&gt;, &lt;code&gt;vbscript:&lt;/code&gt; and &lt;code&gt;blob:&lt;/code&gt; are refused, and so is any URI with a fragment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bad client or redirect URI is shown as an error page, never redirected to.&lt;/strong&gt; Redirecting there would make the server an open redirector. Any other error goes back to the client as &lt;code&gt;?error=&lt;/code&gt; the way RFC 6749 says.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only two grant types&lt;/strong&gt;: &lt;code&gt;authorization_code&lt;/code&gt; and &lt;code&gt;refresh_token&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it cost
&lt;/h2&gt;

&lt;p&gt;One more surface to own. An API key is one table and one header check. This is an authorization server with eight endpoints, a consent screen in the app, and a Connected apps page. My own review notes had a row for it: the MCP flow pointed at a consent page that did not exist yet. That was fixed by building the page at &lt;code&gt;/oauth/consent&lt;/code&gt;, after which the grant shows in Connected apps.&lt;/p&gt;

&lt;p&gt;For an MCP server people add to Claude by hand, I would pay it again. The person never touches a secret, sees exactly which boards they handed over, and has one Revoke button.&lt;/p&gt;

&lt;p&gt;If you run an MCP server, which do your users hold today: an OAuth grant or a key they pasted?&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>oauth</category>
      <category>security</category>
      <category>ai</category>
    </item>
    <item>
      <title>One zod schema per route: Express handler, OpenAPI and TS client</title>
      <dc:creator>Panth Patel</dc:creator>
      <pubDate>Tue, 20 Oct 2026 23:36:09 +0000</pubDate>
      <link>https://dev.to/panthpatel/one-zod-schema-per-route-express-handler-openapi-and-ts-client-4m7f</link>
      <guid>https://dev.to/panthpatel/one-zod-schema-per-route-express-handler-openapi-and-ts-client-4m7f</guid>
      <description>&lt;p&gt;Each endpoint is declared once, with a zod schema for what comes in and what goes out. From that one declaration you get three things: the handler Express serves, the OpenAPI JSON for your docs, and a TypeScript client for the frontend.&lt;/p&gt;

&lt;p&gt;The constraint: the declaration is not a server. &lt;a href="https://jsr.io/@panth977/routes" rel="noopener noreferrer"&gt;@panth977/routes&lt;/a&gt; only describes routes. Serving them is a separate handler, &lt;a href="https://jsr.io/@panth977/routes-express" rel="noopener noreferrer"&gt;@panth977/routes-express&lt;/a&gt; for Express, or one you write for another framework.&lt;/p&gt;

&lt;p&gt;I'm Panth, and I lead the software team at Oizom. &lt;code&gt;@panth977/routes&lt;/code&gt; is one of my open-source libraries on JSR. This post walks through it with code: middleware, an HTTP route, an SSE route, and the calls that turn a folder of routes into docs and a client.&lt;/p&gt;

&lt;h2&gt;
  
  
  What one declaration gives you
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;From the route's schemas&lt;/th&gt;
&lt;th&gt;Through&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A typed handler, served by Express&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ROUTES.execute&lt;/code&gt; + &lt;code&gt;@panth977/routes-express&lt;/code&gt; (or your own handler)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAPI JSON&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ROUTES.getRouteDocJson&lt;/code&gt;, built on &lt;a href="https://www.npmjs.com/package/zod-openapi" rel="noopener noreferrer"&gt;zod-openapi&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A TypeScript client for the frontend&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ROUTES.CodeGen.genTsCode&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Because every route is already declared with types, the docs don't need a second source. The OpenAPI JSON can serve a docs UI, and the same JSON generates the frontend's code. Nothing is written twice.&lt;/p&gt;

&lt;p&gt;Routes are built the same way as &lt;a href="https://jsr.io/@panth977/functions" rel="noopener noreferrer"&gt;@panth977/functions&lt;/a&gt;, so you keep its &lt;code&gt;wrappers&lt;/code&gt;, &lt;code&gt;context&lt;/code&gt;, &lt;code&gt;static&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;namespace&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt;, and input/output schemas. The &lt;a href="https://panth.vardayinitech.in/blog/how-to-implement-type-safe-functions-in-javascript" rel="noopener noreferrer"&gt;functions post&lt;/a&gt; covers those.&lt;/p&gt;

&lt;h2&gt;
  
  
  Middleware
&lt;/h2&gt;

&lt;p&gt;A middleware declares its own request schema and its security rule. Both end up in the docs of every route that uses it.&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;const&lt;/span&gt; &lt;span class="nx"&gt;DecodedStateKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FUNCTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DefaultContextState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CreateKey&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;decodeToken&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Decoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;global&lt;/span&gt;&lt;span class="dl"&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;authorize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;JwtAuth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;apiKey&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// this rules will be used for documentation of routes&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorize ** /users/login **.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-auth-token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MiddlewareRequest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="c1"&gt;// this schema will be used to address route request schema, in documentation&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-auth-token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;passthrough&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MiddlewareResponse&lt;/span&gt;&lt;span class="p"&gt;({}),&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-auth-token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
      &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
      &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-token&lt;/span&gt;&lt;span class="dl"&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;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;decodeToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="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;decoded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;createHttpError&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Unauthorized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Token not found / Token got Expired / Invalid Token!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// set state to context for others to make use of it!&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DecodedStateKey&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one looks for the token in three headers (&lt;code&gt;x-auth-token&lt;/code&gt;, then &lt;code&gt;authorized&lt;/code&gt;, then &lt;code&gt;x-token&lt;/code&gt;), decodes it, and throws a 401 if that fails. On success it stores the decoded token in the request's context under &lt;code&gt;DecodedStateKey&lt;/code&gt;. Any route that lists &lt;code&gt;authorize&lt;/code&gt; can read it back from there, which is what the next example does.&lt;/p&gt;

&lt;h2&gt;
  
  
  An HTTP route
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/profile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Get user profile.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;getFriends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&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;false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;noOfProjects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;static&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Select * from users where id = ? LIMIT 1`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DecodedStateKey&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;userId&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;createHttpError&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Given user was not found in db, could be because someone has deleted the user.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getFriends&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&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;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="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 route reads the user id that &lt;code&gt;authorize&lt;/code&gt; stored, runs a query declared once in &lt;code&gt;static&lt;/code&gt;, and returns a row. The &lt;code&gt;getFriends&lt;/code&gt; query parameter arrives as the string &lt;code&gt;'true'&lt;/code&gt; or &lt;code&gt;'false'&lt;/code&gt;, with &lt;code&gt;'false'&lt;/code&gt; as the default, because that is what the schema says.&lt;/p&gt;

&lt;p&gt;Compared with &lt;code&gt;@panth977/functions&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt; becomes &lt;code&gt;request&lt;/code&gt; and &lt;code&gt;output&lt;/code&gt; becomes &lt;code&gt;response&lt;/code&gt;. The request's &lt;code&gt;path&lt;/code&gt;, &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;headers&lt;/code&gt; and &lt;code&gt;body&lt;/code&gt; arrive in &lt;code&gt;func&lt;/code&gt; as named arguments. The response has &lt;code&gt;headers&lt;/code&gt; and &lt;code&gt;body&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ROUTES.z.HttpRequest&lt;/code&gt; and &lt;code&gt;ROUTES.z.HttpResponse&lt;/code&gt; stand in for &lt;code&gt;z.object&lt;/code&gt; so the types stay exact. You can still pass your own. For the docs, a route also takes &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, &lt;code&gt;summary&lt;/code&gt;, &lt;code&gt;security&lt;/code&gt;, &lt;code&gt;reqMediaTypes&lt;/code&gt; and &lt;code&gt;resMediaTypes&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Server-Sent Events route
&lt;/h2&gt;

&lt;p&gt;An SSE route is an async generator. This one yields the request's logs 30 at a time until there are none left.&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;getLogs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;systemAuthorized&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/logs/{requestId}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SseRequest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SseResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;z&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;x&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="p"&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;logs&lt;/span&gt; &lt;span class="o"&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;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;do&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;result&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;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SELECT * FROM logs where id = ? ORDER BY ts OFFSET ? LIMIT 30`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="nx"&gt;logs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;
      &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;
  
  
  Chaining middleware instead
&lt;/h2&gt;

&lt;p&gt;If you prefer &lt;code&gt;app.get('/profile', …)&lt;/code&gt; with middleware chained in front, &lt;code&gt;ROUTES.Endpoint&lt;/code&gt; gives you that shape.&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;const&lt;/span&gt; &lt;span class="nx"&gt;initMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&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;userAuthMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&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;thirdPartyAuthMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&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;EndpointFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;addMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initMiddleware&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;endpoints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userAuthorized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EndpointFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userAuthMiddleware&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;thirdPartyAuthorized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EndpointFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;thirdPartyAuthMiddleware&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;getProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAuthorized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/profile&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;getProfile3rdParty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thirdPartyAuthorized&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;/api/{userId}/profile&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;h2&gt;
  
  
  From a folder of routes to OpenAPI
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ROUTES.getEndpointsFromBundle&lt;/code&gt; takes a module's exports and keeps only the endpoints (HTTP and SSE), dropping helpers and middleware. The detailed types are lost at this step, which is what you want when you are serving or filtering every route at once.&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;// --- routes/index.ts ---&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./m.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// middleware&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./a.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// route1, route2, and a helper variable&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./b.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// route3, route4 (SSE), and a helper function&lt;/span&gt;
&lt;span class="c1"&gt;// --- server.ts ---&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;routes_&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./routes/index.ts&lt;/span&gt;&lt;span class="dl"&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;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEndpointsFromBundle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;routes_&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// strong type will be lost&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// {route1: ..., route2: ..., route3: ..., route4: ...}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the OpenAPI JSON, with routes tagged &lt;code&gt;internal-apis&lt;/code&gt; left out:&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;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEndpointsFromBundle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./routes/index.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;excludeTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;internal-apis&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;OpenApiJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRouteDocJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My Apis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.0.1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Serving a route
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ROUTES.execute&lt;/code&gt; runs one endpoint through lifecycle hooks you supply. This is the seam where a framework plugs in. The example uses Express; &lt;a href="https://github.com/panth977-packages/routes-express" rel="noopener noreferrer"&gt;routes-express&lt;/a&gt; is the full reference handler.&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;type&lt;/span&gt; &lt;span class="nx"&gt;MyFrameworkOpt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Express&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MyEndpointExecutionLifeCycle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LifeCycle&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MyFrameworkOpt&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="p"&gt;{...},&lt;/span&gt;
    &lt;span class="nf"&gt;onStatusChange&lt;/span&gt;&lt;span class="p"&gt;({...})&lt;/span&gt; &lt;span class="p"&gt;{...},&lt;/span&gt;
    &lt;span class="nf"&gt;onExecution&lt;/span&gt;&lt;span class="p"&gt;({...})&lt;/span&gt; &lt;span class="p"&gt;{...},&lt;/span&gt;
    &lt;span class="nf"&gt;onResponse&lt;/span&gt;&lt;span class="p"&gt;({...})&lt;/span&gt; &lt;span class="p"&gt;{...},&lt;/span&gt;
    &lt;span class="nf"&gt;onComplete&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;app&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;/getUser&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;FUNCTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DefaultContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;ROUTES&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getUserEndpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;lc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MyEndpointExecutionLifeCycle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;finally&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&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;
  
  
  The TypeScript client
&lt;/h2&gt;

&lt;p&gt;The last output comes from the OpenAPI JSON. &lt;code&gt;ROUTES.CodeGen&lt;/code&gt; turns it into frontend code with a typed call for every API. Here it is served from an endpoint:&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;app&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;/code-gen&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;FUNCTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DefaultContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CodeGen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&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;finish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CodeGen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;genTsCode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/x.typescript&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Something went wrong!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schema is written once, and the handler, the docs and the client are all read from it.&lt;/p&gt;

&lt;p&gt;The rest of the packages are at &lt;a href="https://jsr.io/@panth977" rel="noopener noreferrer"&gt;jsr.io/@panth977&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Where do your API docs come from today: the route code itself, or a spec someone keeps in step by hand?&lt;/p&gt;

</description>
      <category>node</category>
      <category>typescript</category>
      <category>openapi</category>
      <category>zod</category>
    </item>
    <item>
      <title>Request-scoped logging in Node.js and Express at Oizom</title>
      <dc:creator>Panth Patel</dc:creator>
      <pubDate>Tue, 20 Oct 2026 23:27:22 +0000</pubDate>
      <link>https://dev.to/panthpatel/request-scoped-logging-in-nodejs-and-express-at-oizom-3hff</link>
      <guid>https://dev.to/panthpatel/request-scoped-logging-in-nodejs-and-express-at-oizom-3hff</guid>
      <description>&lt;p&gt;When an API call fails with a 500 in production, I want one id from the response and, behind it, every log line that request wrote, in order. At Oizom every request gets a context id, every function logs through &lt;code&gt;context.log&lt;/code&gt;, and the logs of a request are only kept when it fails or the frontend asks for them.&lt;/p&gt;

&lt;p&gt;I'm Panth, and I lead the software team at Oizom, where our devices send tens of thousands of air-quality readings to the cloud every minute. This post is how that logging works in our Node.js and Express backends, with the code. Hosted logging services can collect all of this, but none of them group logs by request out of the box, and running a separate service per request is not an option.&lt;/p&gt;

&lt;h1&gt;
  
  
  The problem
&lt;/h1&gt;

&lt;p&gt;A client makes a request and the API answers with a 500. To debug it you would normally try to recreate the bug, probably by logging in to the frontend as that user. The faster route is to keep the logs of every request that answered 500, keyed by its &lt;code&gt;requestId&lt;/code&gt;, and read them.&lt;/p&gt;

&lt;h1&gt;
  
  
  The idea
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;Every time a request comes in, generate a UUID and include it in the response headers. Whenever anything fails, the API sends a &lt;code&gt;requestId&lt;/code&gt; that the client can show, in a sandbox view or similar.&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;context&lt;/code&gt; object (with &lt;code&gt;requestId&lt;/code&gt; as &lt;code&gt;context.id&lt;/code&gt;) that includes a method &lt;code&gt;context.log&lt;/code&gt;, which works like &lt;code&gt;console.log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When creating the &lt;code&gt;context&lt;/code&gt;, ensure there is an implementation of &lt;code&gt;context.log&lt;/code&gt; where you can access &lt;code&gt;context.id&lt;/code&gt;. This allows you to group the logs by &lt;code&gt;requestId&lt;/code&gt; effectively.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Solution
&lt;/h1&gt;

&lt;p&gt;My libraries &lt;a href="https://jsr.io/@panth977/functions" rel="noopener noreferrer"&gt;@panth977/functions&lt;/a&gt; and &lt;a href="https://jsr.io/@panth977/routes" rel="noopener noreferrer"&gt;@panth977/routes&lt;/a&gt; do this out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create Context
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create Context like following&lt;/span&gt;
&lt;span class="nx"&gt;app&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;/v1/user/:userId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// either set id, to string or leave it null for auto generate UUID&lt;/span&gt;
    &lt;span class="nx"&gt;DefaultContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forTask&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="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;res&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="s2"&gt;finish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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 task started!&lt;/span&gt;&lt;span class="dl"&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// pass context to all the functions, for access to context.log&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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 task completed!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// OR&lt;/span&gt;
&lt;span class="c1"&gt;// use @panth977/routes api to skip the process of creating functions!&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET_USER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;WebsiteAuthValidator&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v2/user/{userId}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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 route started!&lt;/span&gt;&lt;span class="dl"&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// pass context to all the functions, for access to context.log&lt;/span&gt;
        &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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 route completed!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Make every function take &lt;code&gt;context&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;That way every function can use &lt;code&gt;context.log&lt;/code&gt;.&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;const&lt;/span&gt; &lt;span class="nx"&gt;getUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FUNCTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AsyncFunction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;()}),&lt;/span&gt;
    &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;...}).&lt;/span&gt;&lt;span class="nf"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Getting user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SELECT * FROM users WHERE id = ? LIMIT 1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Found user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="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;
  
  
  Capture &lt;code&gt;context.log&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Every call to &lt;code&gt;context.log&lt;/code&gt; runs your subscribed callback.&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;const&lt;/span&gt; &lt;span class="nx"&gt;unsub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DefaultContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// send the log to socket &lt;/span&gt;
    &lt;span class="c1"&gt;// store to database&lt;/span&gt;
    &lt;span class="c1"&gt;// write to a file!&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// incase you like to unsubscribe!&lt;/span&gt;
&lt;span class="nf"&gt;unsub&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, &lt;code&gt;context.log&lt;/code&gt; doesn't write to the console at all, so add &lt;code&gt;console.log&lt;/code&gt; in the callback if you want it.&lt;/p&gt;

&lt;h1&gt;
  
  
  How we use it at Oizom
&lt;/h1&gt;

&lt;p&gt;At Oizom our devices send calibrated air-quality data to the cloud, tens of thousands of entries every minute, and clients read their live data through our APIs. With that many requests, debugging anything in production by reading a shared log stream was close to impossible.&lt;/p&gt;

&lt;h4&gt;
  
  
  What I wanted
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Keep logs only for failed requests, so successful ones don't clutter the logs database.&lt;/li&gt;
&lt;li&gt;A way to list the request IDs made by &lt;code&gt;user&lt;/code&gt; from the website, &lt;code&gt;user&lt;/code&gt; from third-party APIs, &lt;code&gt;device&lt;/code&gt; from the device itself, and &lt;code&gt;system-{type}&lt;/code&gt; for any internal system calls like cron jobs, reports, alerts, etc.&lt;/li&gt;
&lt;li&gt;The full logs for any one request ID.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  What I built
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Define a log payload
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;ids&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;msg&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;ts&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="nl"&gt;line&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many logs belong to the same ID, so why can one log belong to many IDs? Most logs do have a single ID, &lt;code&gt;context.id&lt;/code&gt;. Multiple IDs are there for filtering: the same log line can be found by request, by user, by organisation or by login session. More on that below.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;A secure socket app with three channels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;/send_log&lt;/code&gt; - every server sends its logs through this channel.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/stream_all_logs&lt;/code&gt; - streams all logs in real time as the servers send them.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/stream_logs&lt;/code&gt; - streams only the logs that match its room (one room per &lt;code&gt;id&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A service that listens to &lt;code&gt;/stream_all_logs&lt;/code&gt; and stores every log. For us that is Cassandra, self-hosted in a container, with a &lt;code&gt;ttl&lt;/code&gt; of a few weeks.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;    &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logs_v2&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;CLUSTERING&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;A service with an HTTP GET endpoint &lt;code&gt;/fetch_logs&lt;/code&gt; that reads logs by &lt;code&gt;id&lt;/code&gt; from Cassandra, ordered, with limit/offset (Cassandra has token-based pagination built in).&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A small frontend with two modes: [ &lt;code&gt;real-time&lt;/code&gt; , &lt;code&gt;database&lt;/code&gt; ]&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A text box for the &lt;code&gt;id&lt;/code&gt; and a submit button.&lt;/li&gt;
&lt;li&gt;Depending on the mode, it connects to the socket &lt;code&gt;/stream_logs&lt;/code&gt; or fetches from &lt;code&gt;/fetch_logs&lt;/code&gt;, with a "fetch next" button for more.&lt;/li&gt;
&lt;li&gt;Each log has an expand button that shows all its &lt;code&gt;ids&lt;/code&gt;, which link to the other ids that share the log.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;That is the logging system. The core is in the backend. Every time a &lt;code&gt;context&lt;/code&gt; is created:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It gets a logs state.&lt;/li&gt;
&lt;li&gt;Every &lt;code&gt;context.log&lt;/code&gt; entry goes into that state.&lt;/li&gt;
&lt;li&gt;On an error, or when something needs to be kept, we set &lt;code&gt;.importance = true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On dispose, if &lt;code&gt;.importance === true&lt;/code&gt;, all the logs are streamed to &lt;code&gt;/send_log&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Logs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ids&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;msg&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;ts&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="nl"&gt;line&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="nl"&gt;important&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LogsStateKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FUNCTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DefaultContextState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CreateKey&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Logs&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Logs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;global&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;stringifyLogs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;LOGS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createStringifyLogger&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;forEachLine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="c1"&gt;// exposing this function for set the importance level.&lt;/span&gt;
    &lt;span class="c1"&gt;// this determines wether logs should be sent to socket or not!&lt;/span&gt;
    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;needToSendLogsToSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FUNCTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;useState&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LogsStateKey&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="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;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;important&lt;/span&gt; &lt;span class="o"&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="c1"&gt;// exposing this function for more versatile use. (link different ids to same log)&lt;/span&gt;
    &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;logToSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FUNCTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;useState&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;ids&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;ts&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="nl"&gt;line&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LogsStateKey&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="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;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stringifyLogs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// set a state on context&lt;/span&gt;
    &lt;span class="nx"&gt;FUNCTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DefaultContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LogsStateKey&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="na"&gt;important&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="c1"&gt;// store all the logs in the state&lt;/span&gt;
    &lt;span class="nx"&gt;FUNCTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DefaultContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// every context.log will be mapped to &lt;/span&gt;
        &lt;span class="c1"&gt;// ids=[context.id] &lt;/span&gt;
        &lt;span class="c1"&gt;// so single id for all the context.log &lt;/span&gt;
        &lt;span class="nf"&gt;logToSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;context&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;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="c1"&gt;// at end, check if it is important, and stream them to socket.&lt;/span&gt;
    &lt;span class="nx"&gt;FUNCTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DefaultContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onDispose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LogsStateKey&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="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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;important&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;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;log&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;)&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;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Socket Log Error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Whenever a user logs in from the website, a third party, a device or a system-{type}, the login session gets its own UUID in the auth token.&lt;/li&gt;
&lt;li&gt;Every request goes through an auth check. That is where I work out what kind of login it is and which categories the request belongs to.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WebsiteAuthValidator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MiddlewareRequest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({}).&lt;/span&gt;&lt;span class="nf"&gt;passthrough&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MiddlewareResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({}).&lt;/span&gt;&lt;span class="nf"&gt;passthrough&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// frontend wants to store logs!&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;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-store-logs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;needToSendLogsToSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt; &lt;span class="c1"&gt;// decode the token from headers.&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;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;createHttpError&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Unauthorized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This is not a client Api, You only have access to client APIs&lt;/span&gt;&lt;span class="dl"&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;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;context&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decoded&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="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Core:User:System:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;decoded&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Core:User:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&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="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Core:User:Org:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decoded&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="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Core:User:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&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="nx"&gt;decoded&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="p"&gt;}&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ExpressStateKey&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="nf"&gt;logToSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DecodedStateKey&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decoded&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;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-store-logs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, when you get logs for &lt;code&gt;id = Core:User:2141&lt;/code&gt;, you see a list of all the API calls made by this user. Each log includes a list of other relevant &lt;code&gt;ids&lt;/code&gt;, and one of them will be &lt;code&gt;context.id&lt;/code&gt;. That way, you can get the &lt;code&gt;requestId&lt;/code&gt;, and by fetching the logs of that &lt;code&gt;requestId&lt;/code&gt;, you get the complete logs for that specific API call.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;When a request fails, we keep its logs.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExpressHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createHttpError&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isHttpError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&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="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ExpressHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ExpressStateKey&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="c1"&gt;// log the request, could be helpfull for recreating error, in debugging.&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Req::&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\t&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Err While Req:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;500 Error at:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&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;// set the importance.&lt;/span&gt;
    &lt;span class="nf"&gt;needToSendLogsToSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// nothing we can do further!&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Err:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Something went wrong!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&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="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;v2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ExpressHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExposedRoutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onError&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  The log viewer
&lt;/h1&gt;

&lt;p&gt;I built the viewer with AI's help: I designed the system and let AI write the boring parts. Here it is:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10t9m0qijyxxa1j7852c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10t9m0qijyxxa1j7852c.png" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Later I needed a list of every email sent to one address, or every SMS sent to one phone number. All it took was a new template for &lt;code&gt;id&lt;/code&gt;. Each of those logs carries the &lt;code&gt;requestId&lt;/code&gt;, so you can see which request sent the email.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe8z4vi8wu80rc3vhnjnm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe8z4vi8wu80rc3vhnjnm.png" width="800" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each log shows 4 things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A toggle button to see other linked &lt;code&gt;ids&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;line&lt;/code&gt; number of the log&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ts&lt;/code&gt; in local time&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;msg&lt;/code&gt;, which is the log message.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvipl0lyabtxdmvj48wup.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvipl0lyabtxdmvj48wup.png" width="799" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The extra &lt;code&gt;ids&lt;/code&gt; are links, so you can jump from a user to a request to an email while reading.&lt;/p&gt;




&lt;p&gt;If you build something like this, build it yourself rather than buying it: the ids you will want to filter by are specific to your system.&lt;/p&gt;

&lt;p&gt;How do you find the logs of one failed request in production today?&lt;/p&gt;

</description>
      <category>node</category>
      <category>express</category>
      <category>logging</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Ticket keys in Firestore: issued once, never reused after delete</title>
      <dc:creator>Panth Patel</dc:creator>
      <pubDate>Tue, 20 Oct 2026 23:10:10 +0000</pubDate>
      <link>https://dev.to/panthpatel/ticket-keys-in-firestore-issued-once-never-reused-after-delete-4h4n</link>
      <guid>https://dev.to/panthpatel/ticket-keys-in-firestore-issued-once-never-reused-after-delete-4h4n</guid>
      <description>&lt;p&gt;I split the ticket id from the ticket key so that tickets could move between boards, and then I took moving tickets out of the product.&lt;/p&gt;

&lt;p&gt;The split stayed. A key like &lt;code&gt;ENG-42&lt;/code&gt; gets pasted into WhatsApp messages, emails, GitHub PR titles and other tickets, and every one of those has to keep pointing at the right ticket for as long as the link exists, including after the ticket is gone.&lt;/p&gt;

&lt;p&gt;I'm Panth, and I lead the software team at Oizom. This is from &lt;a href="https://github.com/Panth977/ticket-tracker" rel="noopener noreferrer"&gt;ticket-tracker&lt;/a&gt;, my open-source board, written to replace a tracker called TeamTrack. Its decisions page lists the choices that are expensive to reverse. This is D4.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chosen&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A generated, immutable &lt;code&gt;ticketId&lt;/code&gt;, a &lt;code&gt;key&lt;/code&gt; field, and a &lt;code&gt;keys&lt;/code&gt; index that keeps old keys as redirects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rejected&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The key as the document id, which is what TeamTrack did with &lt;code&gt;TICKET_ID&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Why&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keys change when a ticket moves. Links in emails, WhatsApp messages, GitHub PRs and other tickets' &lt;code&gt;#refs&lt;/code&gt; must keep working after that.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So a ticket lives at &lt;code&gt;boards/{boardId}/tickets/{ticketId}&lt;/code&gt;, and &lt;code&gt;ENG-42&lt;/code&gt; is a field on it. The key also gets a document of its own at the top 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="c1"&gt;// keys/{ticketKey}: '#ENG-42' → the ticket it names&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;KeyIndexSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;ticketId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TicketIdSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;boardId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BoardIdSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="cm"&gt;/** The key itself, equal to the doc id. */&lt;/span&gt;
  &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TicketKeySchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;deleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;current&lt;/code&gt; is the redirect: an old key's row can name the key the ticket has now. Exact-key search follows one hop of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the move was dropped
&lt;/h2&gt;

&lt;p&gt;Phase 1 built moving a ticket to another board: copy the ticket, its thread, activity and files to the other board, give it a new key, and redirect the old one.&lt;/p&gt;

&lt;p&gt;Decision D13 took it out. Moving is machinery (option mapping per field, assignees who aren't on the new board, a queued copy job, the key redirects) for something that happens rarely in a personal tool, and it was the most dangerous write in the system. Now a ticket stays on the board it was created on. To connect work across boards, you link the tickets or &lt;code&gt;#reference&lt;/code&gt; them. A ticket filed on the wrong board gets closed and filed again, and the new one can reference the old.&lt;/p&gt;

&lt;p&gt;So the reason written next to D4 no longer happens. Nothing writes a redirect today. The id and the key stayed separate anyway, because the same split does three other jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. References store the id, not the key
&lt;/h2&gt;

&lt;p&gt;When you type &lt;code&gt;#ENG-42&lt;/code&gt; in a comment, the editor stores a &lt;code&gt;ticketRef&lt;/code&gt; node with the ticket's id. The key in that node is only a hint for rendering:&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;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ticketRef&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;ticketId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;refs&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;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ticket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keyOf&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="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&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 server derives each message's &lt;code&gt;refs&lt;/code&gt; as a list of ticket ids. Mentions of people work the same way: a mention stores a uid and shows the current display name, so a rename never breaks it. TeamTrack stored mentions as &lt;code&gt;[@Name](mailto:…)&lt;/code&gt; links in Markdown and keyed assignees by email, so a change of address was a data migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A key is issued once
&lt;/h2&gt;

&lt;p&gt;Keys come from a counter on the board document, inside the same transaction that creates the ticket. Not from a query for the highest number:&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;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextNumber&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nextNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ticketIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;ticketIds&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;ticketId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ticketKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&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;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;keyRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ticketId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;boardId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;key&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tx.create&lt;/code&gt; fails if the document already exists. A key that was ever handed out, whether it now points at a live ticket, a deleted one or (once) a moved one, can't be overwritten. The transaction fails instead.&lt;/p&gt;

&lt;p&gt;The price is in the comment above that code: the board document takes about one sustained write a second, so one new ticket per second per board. Bulk creation takes a range with a single counter bump.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Delete leaves a tombstone
&lt;/h2&gt;

&lt;p&gt;Deleting a ticket removes it from the links and references of other tickets, takes it off watchers' lists, deletes the ticket document and its thread, and keeps the key:&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;// Tombstone: kept forever, never reissued.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tomb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KeyIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;k&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;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;deleted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ticketId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;boardId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;deleted&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="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyRefs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tomb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, &lt;code&gt;#ENG-42&lt;/code&gt; in an old comment says the ticket was deleted. With the key as the document id, deleting the ticket deletes the only record that the key was ever used. If that key were ever issued again, every old link would open the new ticket as if nothing had happened. The comment in the schema says it more directly: the row is kept "so an old link says 'this ticket is gone' instead of landing on somebody else's work".&lt;/p&gt;

&lt;p&gt;Deleting a whole board does the same for every ticket on it, and its board key stays claimed too. Board keys like &lt;code&gt;ENG&lt;/code&gt; are claimed across the whole app, first come first served, like a username. That is what lets &lt;code&gt;/t/ENG-42&lt;/code&gt; and &lt;code&gt;#ENG-42&lt;/code&gt; mean one ticket without saying whose board it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who reads the index
&lt;/h2&gt;

&lt;p&gt;Everything that starts from a key goes through &lt;code&gt;keys/&lt;/code&gt;, not through a board:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the REST and MCP doors, when an agent or a script asks for &lt;code&gt;ENG-42&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the GitHub hook, which finds keys in branch names, PR titles, commit messages and comments, and posts a line in each ticket's thread&lt;/li&gt;
&lt;li&gt;Markdown coming in through REST or MCP, where &lt;code&gt;#ENG-42&lt;/code&gt; is turned into a ticket id before it is stored&lt;/li&gt;
&lt;li&gt;the search box, for an exact key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A tombstoned key resolves to nothing for all of them. Over REST and MCP it is a 404, and so is a key on a board the caller can't read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost
&lt;/h2&gt;

&lt;p&gt;One extra document per ticket, and one extra read whenever something arrives by key instead of by id: a pasted key, a webhook, a search. Links between tickets store only the id, so finding one looks on the same board first, which is almost always right, and falls back to the &lt;code&gt;keys&lt;/code&gt; index, whose rows carry the board.&lt;/p&gt;

&lt;p&gt;What I'd keep from this for any system with human-readable ids: let the name be a field, give it its own index, issue it once, and never delete the index row. The move that justified D4 is gone, and the split still earns its place on delete alone.&lt;/p&gt;

&lt;p&gt;Have you ever reused an id after a delete, and found out from a link that opened the wrong thing?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>firebase</category>
      <category>database</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Our coding agent prompts live in the ticket, reviewed like PRs</title>
      <dc:creator>Panth Patel</dc:creator>
      <pubDate>Tue, 20 Oct 2026 23:00:29 +0000</pubDate>
      <link>https://dev.to/panthpatel/our-coding-agent-prompts-live-in-the-ticket-reviewed-like-prs-4f2g</link>
      <guid>https://dev.to/panthpatel/our-coding-agent-prompts-live-in-the-ticket-reviewed-like-prs-4f2g</guid>
      <description>&lt;p&gt;Every prompt my team writes to our coding agent is stored in the ticket that asked for it. The ticket thread is the only way a developer can talk to the agent, so there is nowhere else for a prompt to go.&lt;/p&gt;

&lt;p&gt;I'm Panth, and I lead the software team at Oizom, where Envizom is the platform that turns data from air-quality and environmental monitors into something cities and industries can act on. This post is about one design goal of our agent pipeline: the prompt is kept with the work, and it can be reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only channel is the thread
&lt;/h2&gt;

&lt;p&gt;A developer writes what they want in a ticket, in plain English, and drags it to &lt;strong&gt;Approved&lt;/strong&gt;. There is no IDE open, no terminal and no prompt window. That ticket text is the whole spec.&lt;/p&gt;

&lt;p&gt;Our orchestrator, orch, gives the ticket its own stack: a branch in every repo, its own backend, drivers, simulator and frontend, a copy of the dev database and a private preview URL. A coding agent does the work inside that stack. When it is done, it posts the preview link into the thread and moves the ticket to &lt;strong&gt;QAQC&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The developer opens the preview, tests it, replies in the same thread and moves the ticket back. The same agent, with the same context, picks up where it left off. Every follow-up is a message in the thread, so every follow-up is stored too.&lt;/p&gt;

&lt;p&gt;When we set out to build this, it was one of five things we wanted from the start:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every prompt should be stored with the work it produced, so we can review and coach how people ask for things.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What one thread holds
&lt;/h2&gt;

&lt;p&gt;By the time a ticket is done, its thread holds the whole exchange, turn by turn:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the thread&lt;/th&gt;
&lt;th&gt;Who writes it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What was asked&lt;/td&gt;
&lt;td&gt;the developer, in the ticket and in each reply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What was built&lt;/td&gt;
&lt;td&gt;the agent, with the preview link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Which preview was tested&lt;/td&gt;
&lt;td&gt;the developer, by replying to that preview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The feedback&lt;/td&gt;
&lt;td&gt;the developer, in the same thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What the turn cost&lt;/td&gt;
&lt;td&gt;posted on every agent turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The PR and commit links&lt;/td&gt;
&lt;td&gt;orch, after the ticket is dragged to &lt;strong&gt;Git Push&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is the shape of a real one. In the walkthrough video I asked for a camera layout change on a device page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ticket   camera layout change on the device page          -&amp;gt; Approved
agent    builds it in the ticket's stack, posts the preview -&amp;gt; QAQC
me       "also handle more than 4 cameras with a horizontal scroll,
          and set up 8 test cameras on this device"         -&amp;gt; back to the agent
agent    does both, the 8 test cameras in the ticket's own
          database copy, posts the preview                  -&amp;gt; QAQC
me       drags it to Git Push
orch     commits, opens and merges the PRs, posts the links,
          waits for CI, deploys to dev, checks health        -&amp;gt; Completed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line of that is in the thread. Anyone can open the ticket and read what was asked, in the words it was asked in, next to what came out of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading prompts like pull requests
&lt;/h2&gt;

&lt;p&gt;Because the prompts are stored, they can be reviewed. I read how each person on the team asks for things and coach them on it. That has been more useful than any prompting guide.&lt;/p&gt;

&lt;p&gt;A prompting guide tells you what good asks look like in general. A ticket thread shows you how one person on your team asked for one change, what the agent built from it, and how many replies it took to get to something they were happy with. You can point at a specific message and talk about it, the way you would point at a line in a pull request.&lt;/p&gt;

&lt;p&gt;The cost line matters here as well. Each agent turn posts its cost into the thread, right where the ask was made. The person asking sees what the ask cost, and it changes how they write the next one. That is on purpose too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it needs one stack per ticket
&lt;/h2&gt;

&lt;p&gt;A stored prompt is only useful if it sits next to the work it produced, and nothing else. That is where per-ticket isolation comes in.&lt;/p&gt;

&lt;p&gt;Each ticket has its own branch in every repo, its own services and its own copy of the dev database. So a ticket's thread is a self-contained record: what was asked, what was built in that stack, what was tested on that preview. A ticket can sit in QA for a week while three others ship, and its history is still in one place, not mixed into anyone else's.&lt;/p&gt;

&lt;p&gt;The thread is also where the rest of the process shows up. After &lt;strong&gt;Git Push&lt;/strong&gt;, no agent is involved: orch commits, merges, opens and merges the pull requests, waits for CI, deploys to the dev VM and checks its health, and posts the PR and commit links back into the same thread. The prompt, the result and the review trail end up in one ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we get from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every prompt the team writes is stored with the ticket it belongs to.&lt;/li&gt;
&lt;li&gt;I can review how people ask for things using their own tickets, not examples from a guide.&lt;/li&gt;
&lt;li&gt;The cost of each ask is visible where it was asked.&lt;/li&gt;
&lt;li&gt;The ticket is the audit trail: what was asked, what was built, what was tested and what feedback was given.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your team uses coding agents, where do the prompts go once the work ships?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I lead the software team at &lt;a href="https://oizom.com" rel="noopener noreferrer"&gt;Oizom&lt;/a&gt;. The full pipeline, including where we drew the line (dev only, human QA, visible cost), is in &lt;a href="https://dev.to/panthpatel/the-ticket-is-the-interface-how-our-developers-ship-without-a-laptop-4of1"&gt;The ticket is the interface&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>productivity</category>
      <category>codereview</category>
    </item>
  </channel>
</rss>
