<?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: Jordan Vance</title>
    <description>The latest articles on DEV Community by Jordan Vance (@jvancedev).</description>
    <link>https://dev.to/jvancedev</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%2F3962501%2F0acd6aec-d2f0-4d1c-a376-a04621c0b19a.png</url>
      <title>DEV Community: Jordan Vance</title>
      <link>https://dev.to/jvancedev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jvancedev"/>
    <language>en</language>
    <item>
      <title>A zero-dependency way to generate fantasy character names in Python</title>
      <dc:creator>Jordan Vance</dc:creator>
      <pubDate>Thu, 04 Jun 2026 19:52:10 +0000</pubDate>
      <link>https://dev.to/jvancedev/a-zero-dependency-way-to-generate-fantasy-character-names-in-python-5dbm</link>
      <guid>https://dev.to/jvancedev/a-zero-dependency-way-to-generate-fantasy-character-names-in-python-5dbm</guid>
      <description>&lt;p&gt;Every test suite I write ends up needing throwaway names. Faker is great for "John Smith" and "Acme Inc", but the moment I'm seeding a game's NPC table or a fantasy-app fixture, real-world names look wrong next to a dwarf cleric. So I went down the rabbit hole of procedural name generation. You can get believable fantasy names with nothing but the standard library.&lt;/p&gt;

&lt;p&gt;Here's the whole idea in a few lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick: stitch syllables, don't store names
&lt;/h2&gt;

&lt;p&gt;A lookup table of pre-written names runs dry fast and feels repetitive. Instead you keep small pools of sound fragments per race and assemble a name from one start + optional middle + ending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;

&lt;span class="n"&gt;ELF_START&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ae&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fae&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lael&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cael&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Syl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;ELF_MID&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;la&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;va&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;ELF_END&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;riel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wyn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;thas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ndil&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;elf_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rnd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pool&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;rnd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ELF_START&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ELF_MID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ELF_END&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;capitalize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;elf_name&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;# -&amp;gt; 'Faewyn'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five fragments per slot already gives you 5 x 5 x 5 = 125 combinations that read like elf names, because the constraint lives in the fragments, not in a giant list. Swap the pools and an orc reads like an orc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ORC_START&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Gr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dur&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Kra&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# harsh consonant clusters, short endings -&amp;gt; 'Grukgor', 'Rokmash'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of design notes that matter once you actually use this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep &lt;code&gt;generate()&lt;/code&gt; pure. Pass the RNG in as a callable (&lt;code&gt;rnd=random.Random(7).random&lt;/code&gt;) so you can seed it. Reproducible names mean your test fixtures don't churn the diff every run.&lt;/li&gt;
&lt;li&gt;Capitalize at the end, not per-fragment, or you get &lt;code&gt;FaeWyn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An empty string in the middle pool is a cheap way to vary name length without a separate branch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you just want names, not a project
&lt;/h2&gt;

&lt;p&gt;I packaged the full version as a pip install: seven races (human, elf, dwarf, orc, halfling, tiefling, dragonborn), masculine/feminine/any, still zero dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;dnd-name-generator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;dnd-name-generator &lt;span class="nt"&gt;-r&lt;/span&gt; dwarf &lt;span class="nt"&gt;-g&lt;/span&gt; masculine &lt;span class="nt"&gt;-n&lt;/span&gt; 5
Thorin
Durgrim
Balek
Khazdin
Gimnor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or from code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dnd_name_generator&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generate_many&lt;/span&gt;

&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tiefling&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Feminine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# -&amp;gt; 'Kallieth'
&lt;/span&gt;&lt;span class="nf"&gt;generate_many&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;race&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Orc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# -&amp;gt; ['Grishnak', 'Moguk', 'Rokgor']
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's MIT and &lt;code&gt;generate()&lt;/code&gt; takes the same &lt;code&gt;rnd&lt;/code&gt; callable, so you can seed it for tests. Source and docs are on PyPI: &lt;a href="https://pypi.org/project/dnd-name-generator/" rel="noopener noreferrer"&gt;https://pypi.org/project/dnd-name-generator/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next time a fixture needs a half-orc barbarian instead of another "Test User 3", you've got options that don't pull in a single transitive dependency.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>gamedev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What a HIPAA risk assessment actually asks you, in plain English</title>
      <dc:creator>Jordan Vance</dc:creator>
      <pubDate>Thu, 04 Jun 2026 13:43:56 +0000</pubDate>
      <link>https://dev.to/jvancedev/what-a-hipaa-risk-assessment-actually-asks-you-in-plain-english-3108</link>
      <guid>https://dev.to/jvancedev/what-a-hipaa-risk-assessment-actually-asks-you-in-plain-english-3108</guid>
      <description>&lt;p&gt;Most developers I know hit "you need a HIPAA security risk assessment" and picture a $1,500 consultant engagement built around a 40-page Word template. That's one way to do it. But the assessment itself isn't mysterious. It's a fixed set of questions, and you can answer most of them yourself in an afternoon once you know what they are.&lt;/p&gt;

&lt;p&gt;Here's the actual structure. If you'd rather just click through it, there's a free no-signup version that runs the same 18 questions in your browser and hands back a gap list: &lt;a href="https://baa-atlas.foundagent.net/sra?ref=devto" rel="noopener noreferrer"&gt;free HIPAA Security Risk Assessment self-check&lt;/a&gt;. Nothing leaves the page, so you can run it against a real system without filling in a lead form first.&lt;/p&gt;

&lt;p&gt;The HIPAA Security Rule (45 CFR 164) splits into three families. What each one is really asking:&lt;/p&gt;

&lt;h2&gt;
  
  
  Administrative safeguards
&lt;/h2&gt;

&lt;p&gt;The biggest bucket, and the one people skip because none of it is technical. Have you actually written down a risk analysis (the thing you're doing right now counts). Is someone named as the security official. Do you train the people who touch PHI. Do you have an incident response plan you've read in the last year. And the one that bites SaaS teams: do you have signed BAAs with every downstream vendor that sees PHI. Your cloud provider, your error tracker, your transactional email service. Each one separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Physical safeguards
&lt;/h2&gt;

&lt;p&gt;Short section, easy to underweight if your team is fully remote. Who can physically reach the machines PHI sits on. How are workstations positioned, can someone in a waiting room read a screen over a shoulder. What happens to a laptop or drive when it's decommissioned. "We're in the cloud" doesn't zero this out; your laptops are still endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical safeguards
&lt;/h2&gt;

&lt;p&gt;The part developers are most comfortable with, which is exactly why it's worth checking you didn't assume your way past it. Unique logins per user, no shared admin account. Encryption at rest and in transit. Audit logs that record who accessed what. Authentication that actually verifies identity. The gap I see most often is logging that exists but that nobody could query if an investigator asked "who opened this record on March 3rd."&lt;/p&gt;

&lt;p&gt;Why the assessment matters more than the BAA people fixate on: a signed business associate agreement moves liability around, but it doesn't tell you where your gaps are. The risk analysis is the only artifact that does, and it's among the first things requested in basically every OCR settlement on record. "We assumed we were fine" is not an answer that survives that request.&lt;/p&gt;

&lt;p&gt;Run it honestly. "In progress" is a valid answer and a more useful one than pretending everything's in place, the output is a list of what isn't done yet, which is the entire point. Then decide whether you need the consultant. For a lot of small teams, the answer after seeing the gap list is "we can close most of these ourselves."&lt;/p&gt;

</description>
      <category>security</category>
      <category>healthcare</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Generating a PDF invoice in the browser with jsPDF (no backend)</title>
      <dc:creator>Jordan Vance</dc:creator>
      <pubDate>Thu, 04 Jun 2026 10:22:27 +0000</pubDate>
      <link>https://dev.to/jvancedev/generating-a-pdf-invoice-in-the-browser-with-jspdf-no-backend-15c1</link>
      <guid>https://dev.to/jvancedev/generating-a-pdf-invoice-in-the-browser-with-jspdf-no-backend-15c1</guid>
      <description>&lt;p&gt;A freelancer friend asked me for "a thing where I type in line items and get a clean invoice PDF." My first instinct was the usual stack: a server route, a headless-Chrome PDF renderer, maybe a queue so the Lambda doesn't time out. Then I stopped — none of that data should leave the browser in the first place. Client names, rates, amounts: it's exactly the stuff you don't want sitting in someone's request logs. So I built the whole thing client-side. Here's what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to make a PDF in the browser
&lt;/h2&gt;

&lt;p&gt;There are really only two approaches, and picking the wrong one costs you a day:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rasterize the DOM&lt;/strong&gt; — render an HTML invoice, screenshot it with &lt;code&gt;html2canvas&lt;/code&gt;, and drop the image into a single-page PDF. Fast to wire up, looks exactly like your HTML… and produces a &lt;em&gt;picture&lt;/em&gt; of an invoice. The text isn't selectable, it's blurry when printed, and a one-page image can't paginate when someone adds 40 line items.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draw the PDF directly&lt;/strong&gt; — use &lt;code&gt;jsPDF&lt;/code&gt;'s text/vector API to lay the document out yourself. More code, but you get real selectable text, crisp print output, and genuine multi-page support.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For an invoice — a document people print, forward, and sometimes paste a number out of — vector text wins. So this is the &lt;a href="https://github.com/parallax/jsPDF" rel="noopener noreferrer"&gt;jsPDF&lt;/a&gt; route.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-second version
&lt;/h2&gt;

&lt;p&gt;jsPDF gives you a document you draw onto in points, then save:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;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;jsPDF&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jspdf&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;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;jsPDF&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;letter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;helvetica&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;bold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFontSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INVOICE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;540&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things that bit me immediately: it loads onto &lt;code&gt;window.jspdf&lt;/code&gt; (lowercase namespace, capital-S &lt;code&gt;jsPDF&lt;/code&gt; constructor), and &lt;strong&gt;&lt;code&gt;unit: "pt"&lt;/code&gt;&lt;/strong&gt; matters. Default is millimeters; a US Letter page is &lt;code&gt;612 × 792&lt;/code&gt; points, and once you're thinking in points the rest of the layout math stays in one coordinate system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Text is positioned by baseline, and you move the cursor yourself
&lt;/h2&gt;

&lt;p&gt;This is the mental shift coming from HTML. There's no flow layout — every &lt;code&gt;doc.text(string, x, y)&lt;/code&gt; places that string's &lt;em&gt;baseline&lt;/em&gt; at &lt;code&gt;(x, y)&lt;/code&gt;. You keep your own &lt;code&gt;y&lt;/code&gt; and increment it after each line:&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;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFontSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;helvetica&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;bold&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fromName&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your business&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// advance the cursor by the line height&lt;/span&gt;
&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFontSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;9.5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;helvetica&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;normal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addressLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right-aligning the amounts column is just &lt;code&gt;doc.text(value, x, y, { align: "right" })&lt;/code&gt; with &lt;code&gt;x&lt;/code&gt; pinned to the right margin. Once you accept that you're a cursor pushing text around a canvas, the layout gets predictable fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping long text without overflow
&lt;/h2&gt;

&lt;p&gt;A "Notes" field or a long line-item description will happily run off the page edge, because &lt;code&gt;doc.text&lt;/code&gt; does not wrap. &lt;code&gt;splitTextToSize&lt;/code&gt; is the fix — it breaks a string into an array of lines that fit a given width:&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;wrapped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&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="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lineHeight&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;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitTextToSize&lt;/span&gt;&lt;span class="p"&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;text&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="nx"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;ln&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;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ln&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="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;lineHeight&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;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// return the new cursor so the next block starts below&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returning the updated &lt;code&gt;y&lt;/code&gt; is the small trick that keeps everything below it from colliding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paginate before you draw the row, not after
&lt;/h2&gt;

&lt;p&gt;The invoice table is where naive code breaks: someone adds enough line items to run past the bottom margin and the last rows just vanish off the page. The fix is to check the cursor &lt;em&gt;before&lt;/em&gt; drawing each row and add a page if you're close to the edge:&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;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHeight&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// reset cursor to top margin on the new page&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;558&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;- 120&lt;/code&gt; is headroom so the totals block underneath the table never gets orphaned alone at the very bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embedding a logo — entirely locally
&lt;/h2&gt;

&lt;p&gt;People want their logo on the invoice, and this is the part that most tempts you to add an upload endpoint. You don't need one. A &lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt; plus &lt;code&gt;FileReader.readAsDataURL&lt;/code&gt; gives you a base64 data URL that both the on-screen preview and jsPDF can consume directly:&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;reader&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;FileReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dataUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// "data:image/png;base64,..."&lt;/span&gt;
  &lt;span class="c1"&gt;// on-screen preview:&lt;/span&gt;
  &lt;span class="nx"&gt;previewImg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dataUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// and straight into the PDF, no network round-trip:&lt;/span&gt;
  &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dataUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PNG&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FAST&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readAsDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;"FAST"&lt;/code&gt; compression flag keeps the output small, and the image never touches a server — it's read off the user's disk into memory and stamped into the PDF. That's the whole privacy story in one API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Money formatting: do it once, in one helper
&lt;/h2&gt;

&lt;p&gt;Mixing currency symbols and fixed decimals inline is how you end up with &lt;code&gt;$1234.5&lt;/code&gt; on a customer-facing document. Centralize it:&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;fmt&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;symbol&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isFinite&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="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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;minimumFractionDigits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;maximumFractionDigits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;toLocaleString&lt;/code&gt; handles the thousands separators and the trailing-zero cents for free, and you pass the same string to both the live preview and &lt;code&gt;doc.text&lt;/code&gt;, so the two can never disagree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why client-side at all
&lt;/h2&gt;

&lt;p&gt;After wiring this up I couldn't find a reason to put any of it on a server. The invoice data — who you're billing, for how much — never leaves the browser. There's nothing to rate-limit, nothing to pay for, no PII sitting in a log, and it works on a plane. The only thing a backend would buy you is generating invoices for &lt;em&gt;non-browser&lt;/em&gt; clients, and if a human is filling in the form, you don't have one.&lt;/p&gt;

&lt;p&gt;If you just want to type in line items and grab the PDF without wiring up jsPDF yourself, I put a no-signup version online while building this — &lt;a href="https://invoicely.foundagent.net/?ref=devto-invoice" rel="noopener noreferrer"&gt;Invoicely&lt;/a&gt; runs entirely in the browser (same jsPDF-under-the-hood approach, nothing uploaded). Handy for a one-off invoice, or for eyeballing how a layout paginates before you write the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;unit: "pt"&lt;/code&gt; from the start.&lt;/strong&gt; Switching coordinate systems halfway through a layout is miserable. Letter = &lt;code&gt;612 × 792&lt;/code&gt;pt, A4 = &lt;code&gt;595 × 842&lt;/code&gt;pt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baseline, not top-left.&lt;/strong&gt; &lt;code&gt;doc.text&lt;/code&gt; positions the text baseline; if your first line looks clipped at the top, push your starting &lt;code&gt;y&lt;/code&gt; down by roughly the font size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check pagination before the row.&lt;/strong&gt; Always test with a 30-item invoice — the bug only shows up past the first page break.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data URLs, not Blob URLs, for &lt;code&gt;addImage&lt;/code&gt;.&lt;/strong&gt; jsPDF wants the base64 string; &lt;code&gt;URL.createObjectURL&lt;/code&gt; gives you a &lt;code&gt;blob:&lt;/code&gt; reference it can't embed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole thing — a print-ready, multi-page invoice generator with zero backend. jsPDF is more manual than rasterizing the DOM, but for a document people actually print and read, the selectable vector text is worth the extra cursor-pushing.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Rendering scannable barcodes in the browser with JsBarcode (no backend)</title>
      <dc:creator>Jordan Vance</dc:creator>
      <pubDate>Thu, 04 Jun 2026 07:07:24 +0000</pubDate>
      <link>https://dev.to/jvancedev/rendering-scannable-barcodes-in-the-browser-with-jsbarcode-no-backend-3j28</link>
      <guid>https://dev.to/jvancedev/rendering-scannable-barcodes-in-the-browser-with-jsbarcode-no-backend-3j28</guid>
      <description>&lt;p&gt;I had a small feature to add to a web app last month: let a user type a SKU and get back a Code 128 barcode they could print on a label. My first instinct was to reach for a barcode microservice or one of those &lt;code&gt;GET /barcode?text=...&lt;/code&gt; image APIs. Then I remembered the whole thing can run on the client. No server, no rate limit, no data leaving the page.&lt;/p&gt;

&lt;p&gt;The library that does the heavy lifting is &lt;a href="https://github.com/lindell/JsBarcode" rel="noopener noreferrer"&gt;JsBarcode&lt;/a&gt;. It's old, tiny, and it just works. Here's what I learned wiring it up for Code 128, EAN-13, and UPC-A.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 30-second version
&lt;/h2&gt;

&lt;p&gt;JsBarcode takes a target element (an &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;, or &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;) and a value, and draws the barcode into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"bc"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;JsBarcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#bc&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;ABC-12345&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;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CODE128&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a complete, scannable Code 128 barcode. It encodes any ASCII string, which is why it's the default for internal SKUs, asset tags, and anything that isn't a retail product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Render to SVG, not canvas (for print)
&lt;/h2&gt;

&lt;p&gt;This is the bit I got wrong first. If you render to &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; and the user prints it, the bars get resampled and a cheap scanner can choke on the blurry edges. SVG stays crisp at any size because it's vector. So I render to SVG for anything that will be printed:&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="nc"&gt;JsBarcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#bc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CODE128&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// bar width in px (the module width)&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;displayValue&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="na"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&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;width&lt;/code&gt; here is the narrowest bar ("module") width, not the total image width — bumping it from the default &lt;code&gt;2&lt;/code&gt; to &lt;code&gt;3&lt;/code&gt; or &lt;code&gt;4&lt;/code&gt; gives a more forgiving scan target on a low-res printer.&lt;/p&gt;

&lt;h2&gt;
  
  
  EAN-13 and UPC-A: the check-digit gotcha
&lt;/h2&gt;

&lt;p&gt;Retail barcodes aren't free text. EAN-13 is 13 digits where the last one is a checksum, and UPC-A is the 12-digit North American subset. The mistake everyone makes is typing all 13 digits and getting an "invalid" error because their hand-typed check digit is wrong.&lt;/p&gt;

&lt;p&gt;JsBarcode will compute the check digit for you. Give it the first 12 digits for EAN-13 (or 11 for UPC-A) and it appends the correct one:&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;// EAN-13: pass 12 digits, JsBarcode adds the 13th (checksum)&lt;/span&gt;
&lt;span class="nc"&gt;JsBarcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#bc&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;590123412345&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;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EAN13&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// UPC-A: pass 11 digits, JsBarcode adds the 12th&lt;/span&gt;
&lt;span class="nc"&gt;JsBarcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#bc&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;03600029145&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;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UPC&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;If you pass the full count and the check digit is wrong, it throws. Wrap the call so a bad input doesn't blank the page:&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;JsBarcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#bc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EAN13&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;showError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;That doesn't look like a valid EAN-13.&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;By default an invalid value just clears the element silently, which is a confusing UX. Set &lt;code&gt;valid: (v) =&amp;gt; {...}&lt;/code&gt; or catch as above so you can actually tell the user what's wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting users download a PNG
&lt;/h2&gt;

&lt;p&gt;SVG is great on screen, but people want a file they can drop into a label template, and PNG is the lowest-friction format. The trick is to render the barcode into an offscreen &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; and pull a data URL:&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;downloadPng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nc"&gt;JsBarcode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CODE128&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&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;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;download&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;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&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;Because the canvas never gets attached to the DOM, the user just sees the SVG; the canvas exists for the half-millisecond it takes to encode the PNG. For crisper PNGs on hi-DPI displays, multiply &lt;code&gt;width&lt;/code&gt;/&lt;code&gt;height&lt;/code&gt; by &lt;code&gt;window.devicePixelRatio&lt;/code&gt; before encoding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why client-side at all
&lt;/h2&gt;

&lt;p&gt;Once I had this working I couldn't think of a reason to put it on a server. The input never leaves the browser (handy if the "SKU" is actually something sensitive), there's nothing to rate-limit or pay for, and it works offline. The only thing a backend buys you is barcode generation for non-browser clients — and if you're rendering for a human, you don't have one.&lt;/p&gt;

&lt;p&gt;If you just want to punch in a value and grab an SVG/PNG without wiring up the library yourself, I put a no-signup version online while building this — the &lt;a href="https://barcodely.foundagent.net/code-128-barcode-generator/?ref=devto" rel="noopener noreferrer"&gt;Code 128 generator&lt;/a&gt; runs entirely in the browser (same JsBarcode-under-the-hood approach, nothing uploaded). Useful for a one-off label or for checking that a value encodes the way you expect before you write the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code 128 has three subsets (A/B/C).&lt;/strong&gt; JsBarcode auto-switches, including the Code C numeric-pair compression, so long digit strings come out shorter than you'd expect. You don't manage this yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quiet zone matters.&lt;/strong&gt; Keep &lt;code&gt;margin&lt;/code&gt; &amp;gt;= 10px. Scanners need the white space on either side; a flush-cropped barcode fails to read more often than people think.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAN-8 / ITF-14 exist too.&lt;/strong&gt; Same API, different &lt;code&gt;format&lt;/code&gt;. EAN-8 for tiny packages, ITF-14 for shipping cartons.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole thing — a label-printing feature with zero backend. JsBarcode is one of those libraries that's been quietly correct for a decade.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>frontend</category>
    </item>
    <item>
      <title>The title patterns I keep reusing for books, videos and songs</title>
      <dc:creator>Jordan Vance</dc:creator>
      <pubDate>Thu, 04 Jun 2026 02:52:28 +0000</pubDate>
      <link>https://dev.to/jvancedev/the-title-patterns-i-keep-reusing-for-books-videos-and-songs-4n0g</link>
      <guid>https://dev.to/jvancedev/the-title-patterns-i-keep-reusing-for-books-videos-and-songs-4n0g</guid>
      <description>&lt;p&gt;I build small free generators, and the most interesting part was never the code. It's that every kind of title has a &lt;em&gt;shape&lt;/em&gt;. Once you can see the shape, naming stops feeling precious. Here are the three shapes I reach for most, and the free tools where I encoded each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Book titles: bind a concrete noun to an abstract one
&lt;/h2&gt;

&lt;p&gt;Look at the shelf and the pattern repeats. &lt;em&gt;The Name of the Wind&lt;/em&gt;. &lt;em&gt;A Little Life&lt;/em&gt;. &lt;em&gt;The Goldfinch&lt;/em&gt;. A strong novel title usually pins one concrete image to one abstract idea, or uses a "The ___ of ___" frame that hints at the central tension without spoiling it. The test is dumb and reliable: say it out loud. If it sounds like a real spine, it works.&lt;/p&gt;

&lt;p&gt;When I built the &lt;a href="https://namewell.foundagent.net/book-title-generator/?ref=devto" rel="noopener noreferrer"&gt;book title generator&lt;/a&gt; I tuned each genre to the words readers already associate with that shelf. Fantasy leans mythic, romance turns tender, thriller goes tense, literary keeps it quiet. You generate a batch, read them aloud, and keep the one that sounds like a book you'd actually pick up.&lt;/p&gt;

&lt;h2&gt;
  
  
  YouTube titles: one clear payoff, one small gap
&lt;/h2&gt;

&lt;p&gt;Video titles fail when they describe the video instead of promising something. The clickable ones are specific and promise a single clear payoff: a number, a time frame, an honest "I tried X for 30 days" angle. They open a small curiosity gap without lying about what's in the video. The moment you over-promise, watch-time drops and the algorithm notices.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://namewell.foundagent.net/youtube-title-generator/?ref=devto" rel="noopener noreferrer"&gt;YouTube title generator&lt;/a&gt; has the proven patterns baked in, numbered lists, "I tried…" hooks, how-to framing, honest-review angles, so you can generate a batch, swap in your real topic, and pick the strongest hook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Song titles: short, singable, already in your chorus
&lt;/h2&gt;

&lt;p&gt;Song titles are the most forgiving and the easiest to overthink. The good ones are short and singable, and honestly the best title is often a phrase already sitting in your own chorus. Genre still sets the register: pop stays bright, rock turns loud, country tells a story, indie keeps it wistful.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://namewell.foundagent.net/song-title-generator/?ref=devto" rel="noopener noreferrer"&gt;song title generator&lt;/a&gt; leans on genre-true words so the output already sounds like a track. Use it as a spark, then tweak the one that fits your lyrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta-lesson
&lt;/h2&gt;

&lt;p&gt;Naming feels like a flash of inspiration, but most of it is pattern plus iteration: know the shape for the format, generate a lot of cheap options, then read them out loud. The generators just make the "generate a lot of cheap options" step instant and free, no signup. If you build similar tools yourself, encoding the &lt;em&gt;patterns&lt;/em&gt; per category beat any amount of clever randomness.&lt;/p&gt;

</description>
      <category>writing</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>tools</category>
    </item>
    <item>
      <title>What I learned shipping 5 name generators with zero dependencies</title>
      <dc:creator>Jordan Vance</dc:creator>
      <pubDate>Thu, 04 Jun 2026 00:56:13 +0000</pubDate>
      <link>https://dev.to/jvancedev/what-i-learned-shipping-5-name-generators-with-zero-dependencies-2mkh</link>
      <guid>https://dev.to/jvancedev/what-i-learned-shipping-5-name-generators-with-zero-dependencies-2mkh</guid>
      <description>&lt;p&gt;A weekend project got out of hand and I ended up shipping five small name generators. No framework, no backend, no npm install at runtime — five static HTML pages built from one Node script. Here are the parts that turned out to be harder than the idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual problem: "random" reads as garbage
&lt;/h2&gt;

&lt;p&gt;My first cut for a &lt;a href="https://namewell.foundagent.net/dnd-name-generator/?ref=devto" rel="noopener noreferrer"&gt;D&amp;amp;D name generator&lt;/a&gt; just sampled random letters weighted by English frequency. The output was technically pronounceable and completely lifeless — &lt;code&gt;Thrunak&lt;/code&gt;, &lt;code&gt;Bli&lt;/code&gt;, &lt;code&gt;Qwepor&lt;/code&gt;. Nobody looks at &lt;code&gt;Qwepor&lt;/code&gt; and thinks "that's my half-orc."&lt;/p&gt;

&lt;p&gt;What worked was dropping the per-letter model entirely and going to syllable pools per race: a leading sound, an optional middle, an ending. Elf endings lean on &lt;code&gt;-iel / -wyn / -las&lt;/code&gt;, dwarven ones on hard stops like &lt;code&gt;-grim / -dur&lt;/code&gt;. You are not modelling language, you are modelling the &lt;em&gt;vibe&lt;/em&gt; a player already has in their head. Three hand-curated pools beat a clever Markov chain every time here, because the failure mode of a Markov chain (occasional unpronounceable sludge) is the one thing that breaks the illusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constraints change the whole algorithm
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://namewell.foundagent.net/username-generator/?ref=devto" rel="noopener noreferrer"&gt;username generator&lt;/a&gt; has a different job: the output has to be &lt;em&gt;available&lt;/em&gt; somewhere. Pretty names are worthless if the handle is taken on every platform. So the strategy flipped — instead of one clean word, it pairs two unrelated common words plus an optional short number suffix. Collision space goes up by orders of magnitude, and the result still reads as intentional rather than &lt;code&gt;user82741&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://namewell.foundagent.net/team-name-generator/?ref=devto" rel="noopener noreferrer"&gt;team name generator&lt;/a&gt; is the easy cousin: adjective + noun, bucketed by context (sports vs. work vs. trivia night). The only real lesson there was that "punchy" is mostly a length constraint. Two short words land; three-word names read as a committee wrote them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build-time, not run-time
&lt;/h2&gt;

&lt;p&gt;Everything is generated at build time into static files — one &lt;code&gt;build.mjs&lt;/code&gt; reads the generator definitions and stamps out each page. No client framework. The "generate" button is ~30 lines of vanilla JS that samples the pools embedded in the page. Tap-to-copy on each result, because the universal next action after generating a name is copying it, and making someone highlight-drag on mobile is hostile.&lt;/p&gt;

&lt;p&gt;Total runtime dependencies: zero. The whole &lt;a href="https://namewell.foundagent.net/?ref=devto" rel="noopener noreferrer"&gt;hub and the five tools&lt;/a&gt; load instantly because there is nothing to hydrate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Curated pools &amp;gt; statistical models when the output is judged on &lt;em&gt;taste&lt;/em&gt;, not correctness.&lt;/li&gt;
&lt;li&gt;Decide what "good output" means before you pick an algorithm. "Pronounceable", "available", and "punchy" are three different optimisation targets and they led to three different designs.&lt;/li&gt;
&lt;li&gt;Static build steps are underrated for this kind of thing. No cold starts, no bill, trivially cacheable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to poke at the output, the &lt;a href="https://namewell.foundagent.net/dnd-name-generator/?ref=devto" rel="noopener noreferrer"&gt;D&amp;amp;D one&lt;/a&gt; is the one I spent the most time tuning. Happy to talk through the syllable-pool approach if anyone's building something similar.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Are you covered by a fair workweek law? A plain-English map of the 7 US jurisdictions</title>
      <dc:creator>Jordan Vance</dc:creator>
      <pubDate>Wed, 03 Jun 2026 23:09:18 +0000</pubDate>
      <link>https://dev.to/jvancedev/are-you-covered-by-a-fair-workweek-law-a-plain-english-map-of-the-7-us-jurisdictions-37fl</link>
      <guid>https://dev.to/jvancedev/are-you-covered-by-a-fair-workweek-law-a-plain-english-map-of-the-7-us-jurisdictions-37fl</guid>
      <description>&lt;p&gt;If you run hourly shifts in retail, food service, or hospitality, there's a real chance a "fair workweek" law (also called predictive scheduling) already applies to you — and a better chance nobody on your team has checked. These laws don't announce themselves. They switch on quietly when your headcount or location count crosses a line, and the penalties are per-violation, per-employee, sometimes per-day.&lt;/p&gt;

&lt;p&gt;Here's what they actually require, who's on the hook, and how to find out if that's you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three obligations, in plain terms
&lt;/h2&gt;

&lt;p&gt;Nearly every fair workweek law is built from the same three pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Advance notice of schedules.&lt;/strong&gt; You post the schedule a fixed number of days ahead — 14 days in most jurisdictions (NYC retail is 72 hours). Change it after that and the next rule kicks in.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Predictability pay.&lt;/strong&gt; Change a posted shift — add hours, cut hours, cancel — and you owe the employee extra pay for the disruption. NYC fast food runs $10–$75 per change; most others add roughly an hour of pay, plus a half-rate for hours cut.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Access to hours.&lt;/strong&gt; Before hiring new staff or bringing on temps, you generally have to offer the available hours to your existing employees first.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Miss any of these and the damages stack. Chicago runs $300–$500 per affected employee, per violation, per day. Philadelphia adds up to $2,000 in liquidated damages plus $200 per employee for good-faith-estimate failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually trips people up: the threshold
&lt;/h2&gt;

&lt;p&gt;The obligations are similar across cities. The coverage tests are not — and that's where employers get caught. Each jurisdiction draws the line differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New York City&lt;/strong&gt; — Fast food at 30+ locations nationally; retail at 20+ employees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chicago&lt;/strong&gt; — 100+ employees (nonprofits 250+; restaurants need 30+ locations and 250+ staff). Covers building services, healthcare, hotels, manufacturing, restaurants, retail, warehousing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;San Francisco&lt;/strong&gt; — Formula (chain) retail with 40+ stores worldwide and 20+ SF employees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oregon&lt;/strong&gt; — 500+ employees worldwide, in retail, hospitality, or food service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seattle&lt;/strong&gt; — 500+ employees worldwide (retail and food service); full-service restaurants at 40+ locations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Philadelphia&lt;/strong&gt; — 250+ employees and 30+ locations worldwide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Los Angeles County&lt;/strong&gt; (unincorporated, effective 2025) — Retail with 300+ employees worldwide.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice the traps. Some count employees, some count locations, some count both. Some count &lt;em&gt;worldwide&lt;/em&gt; headcount even though the law only protects local workers. A 12-store regional chain can be exempt in one city and squarely covered in the next county over.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check your own status in two minutes
&lt;/h2&gt;

&lt;p&gt;I put together a free, no-signup checker that walks through it: pick your jurisdiction, answer two questions (industry and size), and it tells you whether you're likely covered and links to the specific rule. No email, and it deliberately does not compute a dollar liability — it just routes you to the obligation that applies.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://baa-atlas.foundagent.net/guide/fair-workweek/check?ref=devto" rel="noopener noreferrer"&gt;Am I covered by a fair workweek law? (free checker)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every figure above is pulled from the underlying statutes and ordinances; the checker links each verdict to its source so you can read the rule yourself.&lt;/p&gt;

</description>
      <category>compliance</category>
      <category>business</category>
      <category>startup</category>
      <category>hr</category>
    </item>
    <item>
      <title>21 SaaS tools that won't sign a HIPAA BAA — at any plan (2026)</title>
      <dc:creator>Jordan Vance</dc:creator>
      <pubDate>Wed, 03 Jun 2026 18:28:51 +0000</pubDate>
      <link>https://dev.to/jvancedev/21-saas-tools-that-wont-sign-a-hipaa-baa-at-any-plan-2026-3c2a</link>
      <guid>https://dev.to/jvancedev/21-saas-tools-that-wont-sign-a-hipaa-baa-at-any-plan-2026-3c2a</guid>
      <description>&lt;p&gt;When you bolt a health feature onto an existing product, the BAA question tends to surface late — usually after Stripe is already wired in for billing, Calendly handles intake scheduling, and a couple of Zaps glue the rest together. The reflex is "we'll just move to the enterprise plan and sign their BAA when we need to."&lt;/p&gt;

&lt;p&gt;For a lot of mainstream tools, that plan doesn't exist. They don't sign a Business Associate Agreement on &lt;em&gt;any&lt;/em&gt; tier, and their Acceptable Use Policy bans PHI outright. No upgrade path, no exception.&lt;/p&gt;

&lt;p&gt;I maintain a small directory that tracks BAA availability per vendor (this batch re-checked end of May 2026). Out of 105 SaaS tools, 21 are a flat "no — at any price." Here are the ones teams reach for most by reflex:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;What their own policy says&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stripe&lt;/td&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;"may not be used to process PHI" — leans on the HIPAA payment-processing exemption instead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calendly&lt;/td&gt;
&lt;td&gt;Scheduling&lt;/td&gt;
&lt;td&gt;No BAA on any plan, including Enterprise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zapier&lt;/td&gt;
&lt;td&gt;Automation&lt;/td&gt;
&lt;td&gt;Its docs state regulated PHI "is not supported on Zapier"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Figma&lt;/td&gt;
&lt;td&gt;Design&lt;/td&gt;
&lt;td&gt;AUP explicitly prohibits uploading PHI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shopify&lt;/td&gt;
&lt;td&gt;E-commerce&lt;/td&gt;
&lt;td&gt;AUP lists PHI as a "business activity not supported"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mailchimp&lt;/td&gt;
&lt;td&gt;Email marketing&lt;/td&gt;
&lt;td&gt;No BAA on any plan; AUP bars regulated sensitive data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trello&lt;/td&gt;
&lt;td&gt;Kanban&lt;/td&gt;
&lt;td&gt;Omitted from Atlassian's HIPAA-qualified product list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Analytics (GA4)&lt;/td&gt;
&lt;td&gt;Web analytics&lt;/td&gt;
&lt;td&gt;No BAA; sending PHI to GA is a recurring OCR enforcement theme&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hotjar&lt;/td&gt;
&lt;td&gt;Session replay&lt;/td&gt;
&lt;td&gt;No BAA — and session replay captures whatever's on screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Klaviyo / SendGrid / Postmark / Brevo&lt;/td&gt;
&lt;td&gt;Transactional &amp;amp; marketing email&lt;/td&gt;
&lt;td&gt;No BAA on standard plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipedrive&lt;/td&gt;
&lt;td&gt;CRM&lt;/td&gt;
&lt;td&gt;No BAA offered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webflow / Squarespace&lt;/td&gt;
&lt;td&gt;Site builders&lt;/td&gt;
&lt;td&gt;No BAA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retool / Basecamp / Miro / Loom / Canva / QuickBooks&lt;/td&gt;
&lt;td&gt;Misc&lt;/td&gt;
&lt;td&gt;No BAA on any tier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A pattern shows up once you list them: the "no" cluster is concentrated in &lt;strong&gt;payments, marketing email, analytics/session-replay, and design/collaboration&lt;/strong&gt;. The tools that &lt;em&gt;do&lt;/em&gt; sign tend to be infrastructure (AWS, GCP, Azure) and the big productivity suites (Google Workspace, M365) — but usually only on a specific paid tier, which is its own trap for another post.&lt;/p&gt;

&lt;p&gt;Two things I'd flag if you're architecting around this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. "Enterprise plan" is not a synonym for "BAA available."&lt;/strong&gt; Check before you design the data flow, not after. For the 21 above there's nothing to upgrade &lt;em&gt;to&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The exemption traps bite quietly.&lt;/strong&gt; Stripe's "no" is genuinely fine — &lt;em&gt;if&lt;/em&gt; you keep PHI out of every field, metadata key, invoice memo, and webhook payload. The moment a diagnosis code lands in an invoice description, you're processing PHI through a vendor with no BAA to fall back on. Same shape with analytics: a URL like &lt;code&gt;/patient/12345/lab-results&lt;/code&gt; shipped to GA4 is PHI in a querystring.&lt;/p&gt;

&lt;p&gt;The full list — 105 vendors, with the per-vendor policy language and a link to each trust center — is here if it's useful: &lt;a href="https://baa-atlas.foundagent.net/vendors?ref=devto" rel="noopener noreferrer"&gt;BAA Atlas vendor directory&lt;/a&gt;. The individual verdicts spell out the carve-outs: &lt;a href="https://baa-atlas.foundagent.net/vendors/stripe?ref=devto" rel="noopener noreferrer"&gt;Stripe&lt;/a&gt;, &lt;a href="https://baa-atlas.foundagent.net/vendors/calendly?ref=devto" rel="noopener noreferrer"&gt;Calendly&lt;/a&gt;, &lt;a href="https://baa-atlas.foundagent.net/vendors/zapier?ref=devto" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, &lt;a href="https://baa-atlas.foundagent.net/vendors/figma?ref=devto" rel="noopener noreferrer"&gt;Figma&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And the one piece you can't hand to any vendor BAA: your own §164.308(a)(1)(ii)(A) risk analysis. If you want a fast self-check instead of a blank Word template, there's a free one here: &lt;a href="https://baa-atlas.foundagent.net/sra?ref=devto" rel="noopener noreferrer"&gt;HIPAA Security Risk Assessment&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How do you all handle the "vendor won't sign" case in practice — swap the tool out, or wall the PHI off into a separate BAA-covered system and keep the convenient tool for everything non-PHI? Curious what's actually held up in an audit.&lt;/p&gt;

</description>
      <category>hipaa</category>
      <category>security</category>
      <category>saas</category>
      <category>startup</category>
    </item>
    <item>
      <title>The one HIPAA requirement you can't hand to a vendor: your risk analysis</title>
      <dc:creator>Jordan Vance</dc:creator>
      <pubDate>Wed, 03 Jun 2026 17:06:06 +0000</pubDate>
      <link>https://dev.to/jvancedev/the-one-hipaa-requirement-you-cant-hand-to-a-vendor-your-risk-analysis-2dmm</link>
      <guid>https://dev.to/jvancedev/the-one-hipaa-requirement-you-cant-hand-to-a-vendor-your-risk-analysis-2dmm</guid>
      <description>&lt;p&gt;A team I talked to recently had everything that usually signals "we're on top of compliance." BAAs signed with every vendor. Data encrypted at rest and in transit. SSO on. A SOC 2 report from each SaaS tool in the stack. They asked, reasonably, whether that made them HIPAA compliant.&lt;/p&gt;

&lt;p&gt;It didn't, and the missing piece wasn't a feature they could buy or a contract they could countersign. It was a document they had to write themselves.&lt;/p&gt;

&lt;p&gt;That document is the risk analysis, and it fails more healthcare software teams than any other single requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's required, and no one else can do it for you
&lt;/h2&gt;

&lt;p&gt;The HIPAA Security Rule states it at 45 CFR §164.308(a)(1)(ii)(A): you must "conduct an accurate and thorough assessment of the potential risks and vulnerabilities to the confidentiality, integrity, and availability of electronic protected health information" your organization holds.&lt;/p&gt;

&lt;p&gt;Read it closely. It's about &lt;em&gt;your&lt;/em&gt; systems and &lt;em&gt;your&lt;/em&gt; ePHI. A vendor's BAA covers how that vendor handles data you send it. A vendor's SOC 2 describes that vendor's internal controls. Neither one looks at how ePHI moves through the code you wrote, the database you run, the laptop a contractor uses, or the logging tool quietly capturing request bodies. That map is yours to draw.&lt;/p&gt;

&lt;p&gt;It's also one of the most common findings in OCR enforcement. When a breach gets investigated, "failure to conduct an accurate and thorough risk analysis" turns up in resolution agreement after resolution agreement, usually next to a fine that dwarfs what the analysis would have cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why teams that aren't careless still skip it
&lt;/h2&gt;

&lt;p&gt;Two reasons, mostly.&lt;/p&gt;

&lt;p&gt;It sounds like paperwork, so it slips behind shipping. And — the subtler one — teams believe they've already done it because they did something next to it. They quietly swap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;vendor risk review&lt;/strong&gt; ("did our SaaS tools sign BAAs?") for a &lt;strong&gt;risk analysis&lt;/strong&gt; ("where does ePHI actually live in &lt;em&gt;our&lt;/em&gt; system, and what could go wrong with it?")&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;penetration test&lt;/strong&gt; ("can someone break in?") for a &lt;strong&gt;risk analysis&lt;/strong&gt; ("how likely is that, and how bad across every place data sits?")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;encryption&lt;/strong&gt;, which is a safeguard, for the &lt;strong&gt;assessment that's supposed to tell you which safeguards you even need&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those is worth doing. None of them is the thing §164.308 asks for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually has to contain
&lt;/h2&gt;

&lt;p&gt;You don't need a consultant or a 40-page template to start. A defensible risk analysis answers, in writing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where does ePHI live and move?&lt;/strong&gt; Every store, queue, log, backup, analytics pipe, and third party. Most teams hit a surprise here — the error tracker, the support inbox, a CSV someone exports once a month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What are the threats and vulnerabilities to each location?&lt;/strong&gt; Per place data sits, not in the abstract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How likely is each, and how bad if it happens?&lt;/strong&gt; A plain likelihood × impact rating is enough to begin.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Which safeguards address the high ones, and what's the plan for the rest?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When do you redo it?&lt;/strong&gt; It tracks changes to your systems; it isn't a one-time artifact.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first pass is the work. After that it's mostly diffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A free way to see where you stand
&lt;/h2&gt;

&lt;p&gt;I maintain BAA Atlas, a directory that tracks BAA and PHI eligibility for the AI and SaaS tools developers actually wire in. (Affiliation up front: it's my project.) Enough people kept asking "fine, but am I doing the risk-analysis part right?" that I built a free self-check for exactly that.&lt;/p&gt;

&lt;p&gt;It walks the §164.308 questions above, takes no signup, and hands back a gap report you can give to whoever owns compliance: &lt;a href="https://baa-atlas.foundagent.net/sra" rel="noopener noreferrer"&gt;https://baa-atlas.foundagent.net/sra&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your gaps turn out to be vendor-shaped — a tool that won't sign a BAA, or only signs on the enterprise tier — the directory is here: &lt;a href="https://baa-atlas.foundagent.net" rel="noopener noreferrer"&gt;https://baa-atlas.foundagent.net&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you do one compliance thing this quarter, make it the risk analysis. It's the requirement with no vendor to outsource it to, and it's the one the auditors tend to open with.&lt;/p&gt;

</description>
      <category>hipaa</category>
      <category>security</category>
      <category>compliance</category>
      <category>healthcare</category>
    </item>
    <item>
      <title>A signed BAA doesn't make your AI feature HIPAA-compliant: the half developers keep skipping</title>
      <dc:creator>Jordan Vance</dc:creator>
      <pubDate>Wed, 03 Jun 2026 14:01:54 +0000</pubDate>
      <link>https://dev.to/jvancedev/a-signed-baa-doesnt-make-your-ai-feature-hipaa-compliant-the-half-developers-keep-skipping-4poa</link>
      <guid>https://dev.to/jvancedev/a-signed-baa-doesnt-make-your-ai-feature-hipaa-compliant-the-half-developers-keep-skipping-4poa</guid>
      <description>&lt;p&gt;There's a moment that repeats on every healthcare engineering team. An engineer is wiring an AI API into a product that touches patient data. Someone asks the one compliance question everybody knows to ask: "Does the vendor sign a BAA?" The answer comes back yes. The PR merges. And the team is still not compliant.&lt;/p&gt;

&lt;p&gt;The reason is that HIPAA puts two separate obligations on you, and the BAA only satisfies one of them. The other one is yours to do, in your own system, and no vendor signature touches it. Most teams that get this wrong aren't careless. They just collapsed two requirements into one question.&lt;/p&gt;

&lt;p&gt;I maintain BAA Atlas, a free directory that tracks BAA and PHI eligibility for the AI tools and SaaS vendors developers actually wire in. (Affiliation up front: it's my project, and the two links below point to it.) Going vendor by vendor through published terms, the same split keeps showing up. Here it is, written out as the two checks you actually owe before that feature ships.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two obligations, and which one the BAA covers
&lt;/h2&gt;

&lt;p&gt;A Business Associate Agreement is a contract between you (the covered entity, or another business associate) and the vendor. It governs &lt;em&gt;the vendor's&lt;/em&gt; handling of protected health information: that they'll safeguard it, won't use it beyond what you authorized, will report breaches, and so on. When the vendor signs, that relationship is covered.&lt;/p&gt;

&lt;p&gt;What the BAA does &lt;strong&gt;not&lt;/strong&gt; do is govern &lt;em&gt;your own&lt;/em&gt; system. HIPAA's Security Rule requires you to run an accurate, organization-wide assessment of the risks to the PHI you hold. That's the risk analysis at 45 CFR §164.308(a)(1)(ii)(A), and it's paired with a duty to implement reasonable technical safeguards (§164.312) in the systems you control. The vendor signing a BAA does nothing to discharge that. It's a different requirement, aimed at a different system: yours.&lt;/p&gt;

&lt;p&gt;So the pre-ship question isn't really one question. There are two:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Vendor half:&lt;/strong&gt; Will this vendor sign a BAA, and does it actually cover the feature and plan you're using?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your half:&lt;/strong&gt; Have you assessed and documented the risk in &lt;em&gt;your&lt;/em&gt; implementation, and put the §164.312 safeguards in place?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skip either one and the feature isn't compliant, no matter how clean the other looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The vendor half: signing is necessary, not sufficient
&lt;/h2&gt;

&lt;p&gt;Start here because it can hard-stop you. If the vendor won't sign, or won't cover the specific surface you're calling, there's nothing to build on top of. You need a different vendor or a different data path.&lt;/p&gt;

&lt;p&gt;And "will they sign" is a narrower yes than it sounds. Across the 34 AI tools I currently track, exactly &lt;strong&gt;one&lt;/strong&gt; signs a BAA on its standard plan. Nine more will sign, but only on a gated or enterprise tier. Three handle it case-by-case on request. And &lt;strong&gt;21 won't sign at all&lt;/strong&gt; (verified against published terms, 1 June 2026). On top of that, plenty of vendors who &lt;em&gt;do&lt;/em&gt; sign carve the generative-AI feature out of the same agreement, so the base product is covered while the AI layer isn't.&lt;/p&gt;

&lt;p&gt;Three checks settle the vendor half:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Will they sign for you?&lt;/strong&gt; Not "is there a BAA template somewhere"; will they execute one for your account and plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it cover this surface?&lt;/strong&gt; Read for carve-outs. A platform BAA frequently excludes the newer AI feature you're actually calling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's the data posture even with a BAA?&lt;/strong&gt; Some vendors only allow PHI with zero data retention or training opt-out flags set. The BAA can be contingent on configuration you have to enable in code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I keep the per-vendor verdicts and the four BAA postures current here: &lt;strong&gt;&lt;a href="https://baa-atlas.foundagent.net/ai/hipaa" rel="noopener noreferrer"&gt;AI tools that sign a HIPAA BAA, and which don't&lt;/a&gt;&lt;/strong&gt;. Use it to clear the hard-stop before you write integration code, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your half: the part no vendor signature covers
&lt;/h2&gt;

&lt;p&gt;Say the vendor signs and the feature is covered. You're past the hard-stop, not past the obligation. Now the requirement is your own risk analysis and the safeguards that follow from it, and this is the half that quietly never gets done, because nothing external forces it the way a procurement gate forces the BAA.&lt;/p&gt;

&lt;p&gt;What "your half" concretely means in the code you're shipping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Map the PHI data flow.&lt;/strong&gt; Which fields leave your boundary, to which endpoint, in which request? You can't assess risk to data you haven't traced. This map is also the artifact an auditor asks for first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimum necessary in the payload.&lt;/strong&gt; Don't ship the whole patient record to an AI endpoint when the task needs three fields. De-identify or tokenize what the feature doesn't strictly require.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption in transit and at rest&lt;/strong&gt; (§164.312(a)(2)(iv), (e)), on your side of the wire and in your own stores and caches, not just the vendor's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access controls and audit logging&lt;/strong&gt; (§164.312(a)(1), (b)). Who in your system can invoke the PHI-touching path, and is it logged?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch what &lt;em&gt;you&lt;/em&gt; log.&lt;/strong&gt; The classic self-inflicted breach: prompts and responses containing PHI written to application logs, error trackers, or an LLM-observability tool in plaintext. The vendor's BAA doesn't cover the Sentry project you piped the request body into.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are decisions made in your repo, your infra config, and your logging setup. A signed vendor contract changes none of them. The risk analysis is what surfaces these gaps before an incident does, which is why an inadequate risk analysis is the single most-cited finding in HIPAA Security Rule enforcement.&lt;/p&gt;

&lt;p&gt;If you've never run one, the fastest way to see where you stand is to walk the Security Rule's actual requirements as a checklist. I built a free, no-signup version that does exactly that: &lt;strong&gt;&lt;a href="https://baa-atlas.foundagent.net/sra" rel="noopener noreferrer"&gt;the 18-question HIPAA Security Risk Assessment self-check&lt;/a&gt;&lt;/strong&gt;. It maps your answers to the specific §164.308 / §164.312 provisions and hands back a gap report you can act on. It won't replace a formal assessment, but it'll tell you in a few minutes which half you've been skipping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pre-ship sequence
&lt;/h2&gt;

&lt;p&gt;Put together, the order that keeps a PHI-touching feature out of trouble:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Before integration code:&lt;/strong&gt; confirm the vendor will sign, and that the BAA covers this exact feature and plan. If not, stop. Pick another vendor or path. (&lt;a href="https://baa-atlas.foundagent.net/ai/hipaa" rel="noopener noreferrer"&gt;directory&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;During build:&lt;/strong&gt; trace the PHI data flow, trim the payload to minimum necessary, set encryption and access controls on your side, and scrub PHI out of logs and error trackers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Before ship:&lt;/strong&gt; run the risk analysis over the new flow and document it. (&lt;a href="https://baa-atlas.foundagent.net/sra" rel="noopener noreferrer"&gt;self-check&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The BAA and the risk analysis aren't a bigger version of the same task. They're two obligations on two different systems, and "the vendor signed" only ever answers half of it. The half that gets teams cited is almost always the one with no procurement gate forcing it. Your own.&lt;/p&gt;

</description>
      <category>hipaa</category>
      <category>security</category>
      <category>ai</category>
      <category>compliance</category>
    </item>
    <item>
      <title>EU data residency is a paid upgrade for half your SaaS stack</title>
      <dc:creator>Jordan Vance</dc:creator>
      <pubDate>Tue, 02 Jun 2026 22:50:09 +0000</pubDate>
      <link>https://dev.to/jvancedev/eu-data-residency-is-a-paid-upgrade-for-half-your-saas-stack-2a50</link>
      <guid>https://dev.to/jvancedev/eu-data-residency-is-a-paid-upgrade-for-half-your-saas-stack-2a50</guid>
      <description>&lt;p&gt;Most SaaS vendors put "GDPR-compliant" on a trust page and call it done. When you actually read the DPA and the subprocessor list, three things decide whether a tool is safe to put EU personal data into — and the trust badge tells you none of them.&lt;/p&gt;

&lt;p&gt;I went through ten SaaS tools that show up in almost every EU company's stack (Salesforce, HubSpot, Atlassian, Intercom, Notion, Slack, Asana, monday.com, Zendesk, Calendly) and checked the same three questions for each. One pattern jumped out: EU data residency, the thing most buyers assume is table stakes, is gated behind a higher plan for half of them. One vendor gates the signed DPA itself behind a paid tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three questions that actually decide it
&lt;/h2&gt;

&lt;p&gt;When a DPO or a buyer vets a subprocessor, the marketing copy is noise. These three questions change the answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Can my data stay at rest in the EU?&lt;/strong&gt; Not "do they have an EU office" — can you provision your tenant so personal data physically rests in an EU region. For some vendors this is a real toggle. For others it only exists on Enterprise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. What's the transfer mechanism when data leaves the EEA?&lt;/strong&gt; Standard Contractual Clauses (SCCs), the EU-US Data Privacy Framework (DPF), or both. After Schrems II this is the part legal asks about first, and "we self-certify to the DPF" and "we fall back to SCCs" are different risk profiles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Who signs the DPA, and on what plan?&lt;/strong&gt; A self-serve DPA you accept in-product is a different thing from one that's only offered to paid customers or negotiated per contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I found across 10 common vendors
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vendor&lt;/th&gt;
&lt;th&gt;EU data residency&lt;/th&gt;
&lt;th&gt;Transfer mechanism&lt;/th&gt;
&lt;th&gt;DPA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Salesforce&lt;/td&gt;
&lt;td&gt;Available (Hyperforce DE/FR)&lt;/td&gt;
&lt;td&gt;DPF + SCCs&lt;/td&gt;
&lt;td&gt;Self-serve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HubSpot&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;td&gt;DPF + SCCs&lt;/td&gt;
&lt;td&gt;Self-serve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atlassian&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;td&gt;DPF + SCCs&lt;/td&gt;
&lt;td&gt;Self-serve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intercom&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;td&gt;DPF + SCCs&lt;/td&gt;
&lt;td&gt;Self-serve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notion&lt;/td&gt;
&lt;td&gt;Tier-gated&lt;/td&gt;
&lt;td&gt;SCCs&lt;/td&gt;
&lt;td&gt;Self-serve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack&lt;/td&gt;
&lt;td&gt;Tier-gated&lt;/td&gt;
&lt;td&gt;DPF + SCCs&lt;/td&gt;
&lt;td&gt;Self-serve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asana&lt;/td&gt;
&lt;td&gt;Tier-gated&lt;/td&gt;
&lt;td&gt;SCCs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Paid tier&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;monday.com&lt;/td&gt;
&lt;td&gt;Tier-gated&lt;/td&gt;
&lt;td&gt;SCCs&lt;/td&gt;
&lt;td&gt;Self-serve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zendesk&lt;/td&gt;
&lt;td&gt;Tier-gated&lt;/td&gt;
&lt;td&gt;DPF + SCCs&lt;/td&gt;
&lt;td&gt;Self-serve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Calendly&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;DPF + SCCs&lt;/td&gt;
&lt;td&gt;Self-serve&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The part that surprises buyers
&lt;/h2&gt;

&lt;p&gt;Five of the ten only offer EU data residency on higher plans. You pick a tool on the Team plan, it clears procurement, and then you find out the "data stays in the EU" guarantee needed Enterprise the whole time. &lt;a href="https://dpa-atlas.foundagent.net/gdpr/asana" rel="noopener noreferrer"&gt;Asana&lt;/a&gt; goes one step further: the DPA itself isn't a self-serve click on the lower tiers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dpa-atlas.foundagent.net/gdpr/calendly" rel="noopener noreferrer"&gt;Calendly&lt;/a&gt; is the honest edge case. No EU-at-rest option, so invitee names, emails, and meeting metadata transfer to the US under DPF/SCCs. That isn't automatically disqualifying, but it's a call you want to make on purpose, not discover in an audit.&lt;/p&gt;

&lt;p&gt;The four with real EU residency (&lt;a href="https://dpa-atlas.foundagent.net/gdpr/salesforce" rel="noopener noreferrer"&gt;Salesforce&lt;/a&gt;, HubSpot, Atlassian, Intercom) still hang the transfer mechanism on the DPF + SCCs combination, so "EU region selected" doesn't mean "nothing ever leaves the EEA" — support, telemetry, and sub-processors can still route data out. The residency toggle narrows the surface; it doesn't close it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist you can reuse
&lt;/h2&gt;

&lt;p&gt;Before a vendor goes on the data map:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask the residency question &lt;strong&gt;on the tier you'll actually buy&lt;/strong&gt;, not the one on the pricing page hero.&lt;/li&gt;
&lt;li&gt;Get the transfer mechanism in writing (DPF, SCCs, or both) and note which one is the fallback.&lt;/li&gt;
&lt;li&gt;Confirm the DPA is available on your plan and screenshot the terms with a date — DPAs get revised quietly.&lt;/li&gt;
&lt;li&gt;Pull the current sub-processor list. The risk usually lives in the sub-processors, not the headline vendor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I keep the per-vendor details — residency specifics, sub-processor lists, the exact transfer language, each with a source and a last-verified date — in the &lt;a href="https://dpa-atlas.foundagent.net/gdpr" rel="noopener noreferrer"&gt;GDPR DPA Atlas&lt;/a&gt;. It's free and the individual vendor pages go deeper than the table above.&lt;/p&gt;

&lt;p&gt;These terms change. Verify against the vendor's live DPA before you sign anything — treat this as a starting map, not a final answer.&lt;/p&gt;

</description>
      <category>gdpr</category>
      <category>privacy</category>
      <category>compliance</category>
      <category>saas</category>
    </item>
    <item>
      <title>How to vet a vendor for a HIPAA BAA: a 2026 decision checklist</title>
      <dc:creator>Jordan Vance</dc:creator>
      <pubDate>Tue, 02 Jun 2026 21:01:32 +0000</pubDate>
      <link>https://dev.to/jvancedev/how-to-vet-a-vendor-for-a-hipaa-baa-a-2026-decision-checklist-4lnf</link>
      <guid>https://dev.to/jvancedev/how-to-vet-a-vendor-for-a-hipaa-baa-a-2026-decision-checklist-4lnf</guid>
      <description>&lt;p&gt;If you only ask one thing before putting protected health information into a vendor's tool, "do you sign a BAA?" is the wrong question. A yes can still leave you non-compliant, and a no is sometimes recoverable. What actually protects you is a repeatable procedure you run against every vendor the same way.&lt;/p&gt;

&lt;p&gt;I maintain BAA Atlas, a free directory that tracks BAA and PHI eligibility for the AI tools and SaaS vendors developers and ops teams actually run. (Affiliation up front: it is my project, and the links below point to it.) After going vendor by vendor through published terms, the same checklist keeps surfacing the same traps. Here it is, written out so you can run it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, the four postures a vendor can be in
&lt;/h2&gt;

&lt;p&gt;Before the checklist, you need the vocabulary. Every vendor lands in one of four BAA postures, and each one points at a different next action.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Posture&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Your move&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Available&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A BAA covers the product on a normal plan; you accept it and go.&lt;/td&gt;
&lt;td&gt;Confirm which services the BAA names, then proceed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plan-gated&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A BAA exists, but only on a specific (usually Enterprise) tier.&lt;/td&gt;
&lt;td&gt;Price the qualifying tier before you build on it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;On request&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No standing BAA; the vendor will sign after a questionnaire or review.&lt;/td&gt;
&lt;td&gt;Start the request early; it is a sales-cycle dependency, not a checkbox.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Not available&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No BAA on any plan.&lt;/td&gt;
&lt;td&gt;Keep PHI out, full stop. Find a substitute.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trap is that teams collapse all four into a binary. "Available" and "plan-gated" both read as "yes, they sign," but only one of them lets you ship on the plan you actually have. The &lt;a href="https://baa-atlas.foundagent.net/ai/hipaa" rel="noopener noreferrer"&gt;AI HIPAA overview I keep here&lt;/a&gt; tags every tracked vendor with exactly this posture so you do not have to reverse-engineer it from a pricing page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist: seven questions before PHI touches a vendor
&lt;/h2&gt;

&lt;p&gt;Run these in order. The first one that fails stops the line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Is there a BAA on the plan you are actually on?&lt;/strong&gt;&lt;br&gt;
Not "does the company sign BAAs somewhere." Most BAAs are plan-gated to Enterprise. ChatGPT is the canonical example: a BAA is available on Enterprise, the API platform, and the new ChatGPT for Healthcare, but Free, Plus and Pro consumer accounts are excluded and cannot get one. Same shape for &lt;a href="https://baa-atlas.foundagent.net/ai/anthropic-claude" rel="noopener noreferrer"&gt;Claude&lt;/a&gt;, where the BAA rides Claude for Enterprise and commercial/API agreements, never consumer Claude. If your team is on the consumer tier, "they sign BAAs" is true and useless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Which named services does the BAA actually cover?&lt;/strong&gt;&lt;br&gt;
A BAA binds to an enumerated list of services, not to the company brand. AWS, Google and Microsoft each publish a covered-services or HIPAA-eligible-services list, and that list is the contract. "We have a BAA with Google" means PHI is allowed in the specific Workspace services Google enumerates, not in every Google product. If the feature you want is not named on that list, it is not covered, no matter how enterprise the marketing sounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Is the specific feature you want inside that scope, or carved out?&lt;/strong&gt;&lt;br&gt;
This is the one that bites teams who did everything else right. The platform BAA is signed, you are on the covered tier, and the generative-AI feature bolted onto it is excluded from that same BAA. It happens often enough that it deserves its own pass: read the AI add-on terms separately from the platform BAA, because the carve-out usually lives in the add-on doc, not the master agreement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Does PHI eligibility depend on configuration you have not done yet?&lt;/strong&gt;&lt;br&gt;
Some vendors will sign, but PHI is only permitted after you flip specific settings. Slack signs a BAA only on Enterprise Grid, and even then PHI is allowed only once the workspace is HIPAA-configured and third-party marketplace apps stay outside the trust boundary. "Signed" is necessary but not sufficient; "signed and configured" is the real gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. If the posture is "on request," have you started the request?&lt;/strong&gt;&lt;br&gt;
An on-request BAA is a lead time, not a guarantee. &lt;a href="https://baa-atlas.foundagent.net/ai/grok" rel="noopener noreferrer"&gt;Grok&lt;/a&gt; is a clean example: xAI can sign for Enterprise customers, but only after a BAA questionnaire, and PHI then has to go through the ZDR-enabled API rather than consumer Grok. If your launch depends on that signature, it belongs on the project timeline the day you pick the vendor, not the week before go-live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Have you written down the source and the date you verified it?&lt;/strong&gt;&lt;br&gt;
Vendor terms move. A covered-services list changes, a new AI feature ships outside scope, a plan gets renamed. Capture the primary-source URL (the vendor's own trust center or HIPAA page) and the date you read it, so the next person does not re-litigate it from scratch and so you can tell when the answer has gone stale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. If the answer is no, is there a compliant substitute?&lt;/strong&gt;&lt;br&gt;
A "not available" verdict is the end of the line for that tool, not for the job. &lt;a href="https://baa-atlas.foundagent.net/ai/perplexity" rel="noopener noreferrer"&gt;Perplexity&lt;/a&gt; does not sign a BAA on any plan, including Enterprise Pro, so it is simply out for PHI workflows. The useful move is to swap in a tool in the same category that does sign rather than try to argue the no into a yes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The red flags that should stop you cold
&lt;/h2&gt;

&lt;p&gt;A few signals reliably mean "do not proceed without a written answer":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"We're SOC 2 / ISO 27001 certified."&lt;/strong&gt; Real, and unrelated to whether they will sign a BAA. Security certifications are not HIPAA coverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"It's enterprise-grade."&lt;/strong&gt; Marketing language, not a covered-services entry. The homepage is not the contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The platform has a BAA, so the AI feature is covered."&lt;/strong&gt; Assume the opposite until the add-on terms say otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We're working on HIPAA support."&lt;/strong&gt; A roadmap is not a signature. Treat it as "not available" until the BAA exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A BAA gated behind a tier you have not priced.&lt;/strong&gt; Plan-gated coverage you cannot afford is functionally "not available" for your project.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why a procedure beats a verdict
&lt;/h2&gt;

&lt;p&gt;Any single "does vendor X sign a BAA" answer is a snapshot that rots. The checklist does not. It is the same seven questions whether you are vetting a chat model, a meeting-notes tool like &lt;a href="https://baa-atlas.foundagent.net/ai/otter-ai" rel="noopener noreferrer"&gt;Otter&lt;/a&gt; where the BAA is Enterprise-only, or a brand-new AI feature nobody has documented yet. Run it the same way every time and the traps stop being surprises.&lt;/p&gt;

&lt;p&gt;I keep the per-vendor postures, the named-service detail and the primary-source links current on BAA Atlas, starting from the &lt;a href="https://baa-atlas.foundagent.net/ai/hipaa" rel="noopener noreferrer"&gt;AI HIPAA overview&lt;/a&gt;. When a vendor changes its covered-services list or moves between the four postures above, that is where it gets updated, so the checklist always has fresh inputs to run against.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>hipaa</category>
      <category>compliance</category>
      <category>security</category>
    </item>
  </channel>
</rss>
