<?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: Alex Radulovic</title>
    <description>The latest articles on DEV Community by Alex Radulovic (@alex_purpleowl).</description>
    <link>https://dev.to/alex_purpleowl</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3724611%2Ff99bb208-f416-4894-a7b1-992e30a43e40.png</url>
      <title>DEV Community: Alex Radulovic</title>
      <link>https://dev.to/alex_purpleowl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alex_purpleowl"/>
    <language>en</language>
    <item>
      <title>We Spent Days Fighting a Zebra Card Printer. So You Don't Have To.</title>
      <dc:creator>Alex Radulovic</dc:creator>
      <pubDate>Thu, 26 Mar 2026 20:09:07 +0000</pubDate>
      <link>https://dev.to/alex_purpleowl/we-spent-days-fighting-a-zebra-card-printer-so-you-dont-have-to-3hg</link>
      <guid>https://dev.to/alex_purpleowl/we-spent-days-fighting-a-zebra-card-printer-so-you-dont-have-to-3hg</guid>
      <description>&lt;p&gt;If you've ever tried to programmatically control a Zebra ZC350 card printer, you already know the pain. And if you haven't, let me save you some time: it's considerable. I want to save you the pain, even if you are a competitor.&lt;/p&gt;

&lt;p&gt;Feed a card. Encode an NFC chip. Print a badge. Eject it. Sounds simple. It wasn't.&lt;/p&gt;

&lt;p&gt;We built an open-source bridge called &lt;a href="https://github.com/purpleowl-io/dazzle" rel="noopener noreferrer"&gt;Dazzle&lt;/a&gt; because a client needed automated card issuance inside a larger workflow, and getting from "nice hardware" to "working software" was much harder than it should have been.&lt;/p&gt;

&lt;p&gt;If you're going to have zebra problems, you might as well answer them with Dazzle. Bonus points if you get the reference.&lt;/p&gt;

&lt;p&gt;At one point we spent hours staring at APDU responses that all came back &lt;code&gt;6900&lt;/code&gt;, trying to figure out whether we were talking to the card, the reader, the SAM, or something undocumented in between.&lt;/p&gt;

&lt;p&gt;So we open-sourced the thing we wish we'd had on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Dazzle Does
&lt;/h2&gt;

&lt;p&gt;Dazzle is a &lt;code&gt;.NET 8&lt;/code&gt; helper that runs as a child process and speaks JSON over &lt;code&gt;stdin&lt;/code&gt;/&lt;code&gt;stdout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Your app, whether it's Node.js, Python, or anything else that can spawn a process and read lines, sends commands like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;feed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;smartcard.connect&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;smartcard.transmit&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;eject&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The helper talks to the Zebra printer and its internal smart-card encoder, then sends structured results back.&lt;/p&gt;

&lt;p&gt;The goal is simple: keep the ugly vendor-specific parts out of your app code.&lt;/p&gt;

&lt;p&gt;A typical flow looks like this:&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;printer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ZebraCardPrinter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;192.168.1.100&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feed&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;readers&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;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReaders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;smartcardConnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;readers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contactless&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;uid&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;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;smartcardTransmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FFCA000000&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Card UID:&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;smartcardDisconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eject&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point is to make card issuance feel like normal application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Built It
&lt;/h2&gt;

&lt;p&gt;At PurpleOwl, we build custom business software for small and mid-sized companies. In this case, the project needed NFC-enabled ID card issuance as part of a larger operational workflow.&lt;/p&gt;

&lt;p&gt;The Zebra ZC350 hardware is solid. The integration story was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Made This Hard
&lt;/h2&gt;

&lt;p&gt;Here are the problems that made us build Dazzle.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The SDK Isn't Distributed Like a Normal Dependency
&lt;/h3&gt;

&lt;p&gt;There is no clean &lt;code&gt;npm install&lt;/code&gt; or &lt;code&gt;dotnet add package&lt;/code&gt; path for the Zebra Card SDK.&lt;/p&gt;

&lt;p&gt;You download a large installer, dig through &lt;code&gt;C:\Program Files\Zebra Technologies\&lt;/code&gt; for DLLs, manually copy them into your project, and add references one by one.&lt;/p&gt;

&lt;p&gt;Every new machine gets the same ritual. No normal dependency management. No clean reproducible setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. USB Discovery Was Surprisingly Fragile
&lt;/h3&gt;

&lt;p&gt;Finding a USB-connected printer should have been a solved problem.&lt;/p&gt;

&lt;p&gt;Instead, the relevant class could live in a separate assembly that was not always loaded, and the property holding the USB address changed names across SDK versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;SymbolicName&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;UsbSymbolicName&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;Address&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;DeviceId&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We ended up using reflection to locate the type, invoke the discovery method, and probe multiple property names until one worked.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Connections Could Fail Without Looking Failed
&lt;/h3&gt;

&lt;p&gt;The TCP connection to the printer can drop while still leaving you with an object that appears usable.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;IsConnected&lt;/code&gt; might look fine right up until the next SDK call hangs, throws from somewhere deep in the stack, or returns nonsense.&lt;/p&gt;

&lt;p&gt;Dazzle wraps operations with reconnection logic because trusting the reported connection state turned out to be a mistake.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. "Card Present" Did Not Mean "Card Ready"
&lt;/h3&gt;

&lt;p&gt;After feeding a card into the encoder station, there is a delay before the PC/SC reader fully recognizes it.&lt;/p&gt;

&lt;p&gt;During that window, the card can show up as present but unpowered. Connect too early and you get &lt;code&gt;RemovedCard&lt;/code&gt; even though the card is physically there.&lt;/p&gt;

&lt;p&gt;So Dazzle waits until the card is both present and powered before trying to connect.&lt;/p&gt;

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

&lt;p&gt;This one cost us the most time.&lt;/p&gt;

&lt;p&gt;We connected to the printer's internal smart-card encoder over PC/SC, decoded the ATR's historical bytes to ASCII, and got:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Cindy0&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Every APDU came back &lt;code&gt;6900&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Read UID? &lt;code&gt;6900&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Authenticate? &lt;code&gt;6900&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Write? &lt;code&gt;6900&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We assumed we were talking to the wrong slot, maybe the SAM instead of the contactless card, so we tried configuration changes, different share modes, transparent RF commands, IOCTLs, and a lot of other dead ends.&lt;/p&gt;

&lt;p&gt;The real answer was stranger: &lt;code&gt;Cindy0&lt;/code&gt; is the Elatec TWN4 reader's virtual slot. It is not the card. It is a command channel that accepts the reader's "Simple Protocol" wrapped in APDUs.&lt;/p&gt;

&lt;p&gt;So standard PC/SC card commands were failing because we were not actually talking to a card.&lt;/p&gt;

&lt;p&gt;The actual contactless card appears on a different logical slot, but only after you use that command channel to search for a tag first.&lt;/p&gt;

&lt;p&gt;That detail exists in the TWN4 PC/SC docs. It is not surfaced clearly in the Zebra integration story.&lt;/p&gt;

&lt;p&gt;Five hours disappeared because a virtual slot was named &lt;code&gt;Cindy0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ships In The Repo
&lt;/h2&gt;

&lt;p&gt;Dazzle includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;ZebraCardHelper&lt;/code&gt;, the &lt;code&gt;.NET&lt;/code&gt; helper that manages printer and encoder communication, APDUs, tunneling, and reconnection logic&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;zebra-card-printer.js&lt;/code&gt;, a Node.js wrapper that handles process lifecycle, JSON messaging, request/response correlation, and timeouts&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;PAINPOINTS.md&lt;/code&gt;, a write-up of the gotchas, race conditions, and undocumented behaviors we ran into&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are doing anything with Zebra card printers and NFC encoding, that pain-points document alone may save you a lot of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why JSON Over stdio
&lt;/h2&gt;

&lt;p&gt;We used JSON over standard input/output because it is simple and portable. Each request has an &lt;code&gt;id&lt;/code&gt;, and each response echoes it back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"connect"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192.168.1.100"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"connected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"feed"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"smartcard.transmit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"apdu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FFCA000000"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"04a23b1a9070809000"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That keeps the integration language-agnostic. Node.js, Python, Go, or anything else that can spawn a child process and read lines can use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who This Is For
&lt;/h2&gt;

&lt;p&gt;If you're building a system that issues NFC-encoded cards, employee badges, access cards, membership cards, or anything similar on Zebra ZC350 hardware, this project is for you.&lt;/p&gt;

&lt;p&gt;It is also for the developer who is already deep in the Zebra SDK and wondering whether the weirdness is their fault.&lt;/p&gt;

&lt;p&gt;It probably is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Open-Sourced It
&lt;/h2&gt;

&lt;p&gt;We're a five-person shop. We build custom ERP, CRM, and PSA systems. We built Dazzle because a client needed it, and we released it because it felt wasteful to let every future developer rediscover the same problems from scratch.&lt;/p&gt;

&lt;p&gt;The hardware is solid. The integration layer does not need to be this punishing.&lt;/p&gt;

&lt;p&gt;If Dazzle saves someone else from losing half a day to &lt;code&gt;Cindy0&lt;/code&gt;, publishing it was worth it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;  GitHub: &lt;a href="https://github.com/purpleowl-io/dazzle" rel="noopener noreferrer"&gt;purpleowl-io/dazzle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  Original article: &lt;a href="https://purpleowl.io/blog/we-spent-days-fighting-a-zebra-card-printer-so-you-dont-hav" rel="noopener noreferrer"&gt;purpleowl.io/blog/we-spent-days-fighting-a-zebra-card-printer-so-you-dont-hav&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it and run into edge cases we haven't covered yet, I'd genuinely love to hear what you found.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>hardware</category>
      <category>nfc</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I added userId and transactionId to every console.log without refactoring</title>
      <dc:creator>Alex Radulovic</dc:creator>
      <pubDate>Wed, 21 Jan 2026 19:44:03 +0000</pubDate>
      <link>https://dev.to/alex_purpleowl/i-added-userid-and-transactionid-to-every-consolelog-without-refactoring-49od</link>
      <guid>https://dev.to/alex_purpleowl/i-added-userid-and-transactionid-to-every-consolelog-without-refactoring-49od</guid>
      <description>&lt;p&gt;Almost every JavaScript server starts the same way.&lt;/p&gt;

&lt;p&gt;You add a few &lt;code&gt;console.log()&lt;/code&gt; calls so you can see what's happening. You deploy. Everything works.&lt;/p&gt;

&lt;p&gt;For a while, this is completely fine.&lt;/p&gt;

&lt;p&gt;The problem is that this version of "fine" only exists while your system is still small.&lt;/p&gt;

&lt;p&gt;As soon as you have multiple users, concurrent requests, and background jobs, logging quietly stops being helpful. Nothing breaks—but nothing is understandable anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Logging Looks Like Right Before It Fails You
&lt;/h2&gt;

&lt;p&gt;A single user action might:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create or update several records&lt;/li&gt;
&lt;li&gt;trigger validations&lt;/li&gt;
&lt;li&gt;enqueue background work&lt;/li&gt;
&lt;li&gt;call external APIs&lt;/li&gt;
&lt;li&gt;finish seconds or minutes later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of that generates logs. Now multiply by hundreds of users doing the same thing at the same time.&lt;/p&gt;

&lt;p&gt;A log like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updating contact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;contactId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is fine—until you see it 10,000 times a day and have no idea which one matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Conversation Every Team Has (and Then Avoids)
&lt;/h2&gt;

&lt;p&gt;Eventually someone says: &lt;em&gt;"We should really fix logging."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What that usually means is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;passing user IDs everywhere&lt;/li&gt;
&lt;li&gt;threading request IDs through layers&lt;/li&gt;
&lt;li&gt;replacing &lt;code&gt;console.log()&lt;/code&gt; with a custom logger&lt;/li&gt;
&lt;li&gt;touching hundreds of files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For small teams, that's not a small task. It's a refactor with no visible feature payoff, so it gets kicked down the road.&lt;/p&gt;

&lt;p&gt;We've done that ourselves.&lt;/p&gt;

&lt;p&gt;So instead of designing a "proper" logging framework, we imposed a hard constraint:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If adopting this requires rewriting the app, we won't use it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Entire Setup (Yes, Really)
&lt;/h2&gt;

&lt;p&gt;At your application entry point:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;replaceConsole&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loggerMiddleware&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@purpleowl-io/tracepack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;replaceConsole&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, after your auth middleware and before your routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;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="nf"&gt;loggerMiddleware&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Two lines.&lt;/p&gt;

&lt;p&gt;You don't change your existing logs. You don't update call sites. You don't teach the team a new API.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Your Logs Look Like After
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contact created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-15T10:23:01.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alex_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"txId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc-789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"contact created"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same code. Different outcome.&lt;/p&gt;

&lt;p&gt;Now when you have this deeper in your codebase:&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;function&lt;/span&gt; &lt;span class="nf"&gt;updateContact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updating contact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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 output automatically becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"info"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alex_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"txId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc-789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"updating contact"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You didn't pass a user ID. You didn't pass a transaction ID. The context followed the execution for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works Without Being Fragile
&lt;/h2&gt;

&lt;p&gt;Under the hood, this uses Node's &lt;code&gt;AsyncLocalStorage&lt;/code&gt; to attach context to the &lt;em&gt;execution path&lt;/em&gt;, not to individual function calls.&lt;/p&gt;

&lt;p&gt;That context survives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;await&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;promises&lt;/li&gt;
&lt;li&gt;database drivers&lt;/li&gt;
&lt;li&gt;network calls&lt;/li&gt;
&lt;li&gt;timers&lt;/li&gt;
&lt;li&gt;background work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once a request starts, everything it touches can log with the same identity—even if the work fans out across multiple async layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Business Context
&lt;/h2&gt;

&lt;p&gt;Sometimes user + transaction isn't enough.&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="k"&gt;import&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@purpleowl-io/tracepack&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;addContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orderId&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="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that point forward, every log in that async chain includes &lt;code&gt;orderId&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background Jobs and Scripts
&lt;/h2&gt;

&lt;p&gt;For non-HTTP code, you can explicitly establish context:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withContext&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@purpleowl-io/tracepack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;withContext&lt;/span&gt;&lt;span class="p"&gt;(&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="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="na"&gt;txId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nightly-job-001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;starting batch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processBatch&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;batch complete&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;Those logs still look exactly like request logs—structured, searchable, and correlated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filtering With jq
&lt;/h2&gt;

&lt;p&gt;All output is clean JSON, one log entry per line:&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;# Filter by transaction&lt;/span&gt;
node app.js | jq &lt;span class="s1"&gt;'select(.txId == "abc-789")'&lt;/span&gt;

&lt;span class="c"&gt;# Filter by user&lt;/span&gt;
node app.js | jq &lt;span class="s1"&gt;'select(.userId == "alex_123")'&lt;/span&gt;

&lt;span class="c"&gt;# Errors only&lt;/span&gt;
node app.js | jq &lt;span class="s1"&gt;'select(.level == "error")'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Get It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i @purpleowl-io/tracepack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;: &lt;a href="https://www.npmjs.com/package/@purpleowl-io/tracepack" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@purpleowl-io/tracepack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/purpleowl-io/tracepack" rel="noopener noreferrer"&gt;https://github.com/purpleowl-io/tracepack&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;We built this because we were tired of reading logs that couldn't answer the questions we actually had.&lt;/p&gt;

&lt;p&gt;If your system is still small enough that logs are readable by default, great. Enjoy it while it lasts.&lt;/p&gt;

&lt;p&gt;If you've crossed the line where concurrency has turned debugging into guesswork, this is a small change that pays off every single time something breaks.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the implementation or tradeoffs.&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>logging</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
