<?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: V G P</title>
    <description>The latest articles on DEV Community by V G P (@mrtinkz).</description>
    <link>https://dev.to/mrtinkz</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%2F3926115%2Fa2f0ea8b-e027-4057-bd8a-40b32977e263.png</url>
      <title>DEV Community: V G P</title>
      <link>https://dev.to/mrtinkz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrtinkz"/>
    <language>en</language>
    <item>
      <title>I built Kobol (Kobol Programming Language) — because code is read far more often than it is written</title>
      <dc:creator>V G P</dc:creator>
      <pubDate>Thu, 04 Jun 2026 12:00:11 +0000</pubDate>
      <link>https://dev.to/mrtinkz/i-built-kobol-because-code-is-read-far-more-often-than-it-is-written-2gae</link>
      <guid>https://dev.to/mrtinkz/i-built-kobol-because-code-is-read-far-more-often-than-it-is-written-2gae</guid>
      <description>&lt;h2&gt;
  
  
  A maintenance window at 2 a.m.
&lt;/h2&gt;

&lt;p&gt;A few years ago I spent a night staring at a function I had written myself, maybe eight months earlier. It was clever. It was dense. It chained four higher-order calls across two lines and folded a reducer into a ternary inside a map. When I wrote it, I felt smart. At 2 a.m., trying to fix a rounding bug in it, I felt nothing of the sort.&lt;/p&gt;

&lt;p&gt;I rewrote it the next morning as a boring, plain loop with names that said what they meant. The bug was obvious once the code stopped showing off. It took me longer to type. It took everyone after me far less time to understand.&lt;/p&gt;

&lt;p&gt;That night stuck with me. We spend a tiny fraction of a program's life writing it, and almost all of the rest reading it: debugging, reviewing, onboarding, patching it at 2 a.m. years later. And yet so much of our tooling optimizes for the act of writing: fewer keystrokes, denser expressions, cleverer symbols. We optimize the cheap part and pay for it in the expensive part.&lt;/p&gt;

&lt;p&gt;Today I'm releasing &lt;strong&gt;Kobol&lt;/strong&gt;, a small, opinionated language that takes the opposite bet.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Kobol (with a K) is inspired by COBOL. It is not COBOL.&lt;/strong&gt; The Kobol language is new and independent, with its own syntax, compiler, and runtime. It isn't a COBOL dialect, isn't source-compatible with COBOL, and isn't affiliated with or endorsed by any COBOL vendor or standards body.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Going back to a very old, very good idea
&lt;/h2&gt;

&lt;p&gt;Here's the question that started it: &lt;em&gt;why do we write programs in syntax that doesn't read like the language we think in?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We had an answer to that once. In 1959, a committee called CODASYL set out to build a programming language that ordinary people, not just specialists, could read. The result was COBOL, and the people behind it were serious computer scientists with a genuinely radical goal for the time: make programs &lt;strong&gt;readable&lt;/strong&gt;. Grace Hopper's earlier work on FLOW-MATIC had already argued that English-like commands were not a gimmick but a feature, and that idea carried straight into COBOL's design. Jean Sammet and the rest of the committee built a language where a statement like &lt;code&gt;ADD 1 TO total&lt;/code&gt; meant exactly what it said, to a programmer and to the accountant reading over their shoulder.&lt;/p&gt;

&lt;p&gt;I want to be clear about something, because the internet loves to dunk on COBOL: those people were right about the important part. The systems they designed have run the world's payrolls, banks, and government benefits for over sixty years. Sixty years. Most of our frameworks don't survive sixty months. That is not an accident of inertia. It's a consequence of a design that prioritized clarity and stability over cleverness. When the original designers talked about the future, they talked about programs that would outlive their authors and still be legible to whoever inherited them. They were thinking about my 2 a.m.&lt;/p&gt;

&lt;p&gt;What they couldn't have planned for was the world the language got chained to: the mainframe, fixed-column punch-card layouts, divisions and paragraphs and ceremony that made sense in 1959 and make a 25-year-old developer bounce off the language today. The good idea got buried under the era it was born in.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I took the idea, and left the era behind
&lt;/h2&gt;

&lt;p&gt;Kobol keeps what was good — English-like, readable, says-what-it-means syntax — and drops everything that tied it to a specific decade of hardware.&lt;/p&gt;

&lt;p&gt;Here's a complete Kobol program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NOTE:
  Hello World in Kobol — the simplest possible program.
  Block comments read like English: NOTE: ... END-NOTE.
END-NOTE

PROGRAM HelloWorld
  VERSION "1.0"

DATA:
  greeting : TEXT = "Hello, World!"

PROCEDURE Main:
  DISPLAY greeting
  DISPLAY "Welcome to Kobol — a modern COBOL-inspired language."
  STOP RUN
END-PROCEDURE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can read that out loud and a non-programmer would mostly follow it. That's the whole point.&lt;/p&gt;

&lt;p&gt;But readable doesn't have to mean toy. Here's real business logic — invoice processing with money, conditions, and error handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PROCEDURE ProcessOneInvoice:
  ADD 1                        TO summary.total-count
  ADD current-invoice.amount   TO summary.total-amount

  IF current-invoice.Paid:
    ADD 1 TO summary.paid-count

  ELSE IF current-invoice.Overdue:
    ADD 1 TO summary.overdue-count
    PERFORM CalculateLateFee
    ADD adjusted-amount TO summary.total-outstanding

  ELSE:
    ADD 1 TO summary.pending-count
    ADD current-invoice.amount TO summary.total-outstanding
  END-IF
END-PROCEDURE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;current-invoice.Paid&lt;/code&gt; and &lt;code&gt;.Overdue&lt;/code&gt;. Those aren't magic. They're &lt;strong&gt;named conditions&lt;/strong&gt; declared on the record itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RECORD Invoice:
  invoice-id    : INTEGER
  customer-name : TEXT(100)
  amount        : MONEY(12.2)
  due-date      : DATE
  paid          : BOOLEAN
  status-code   : TEXT(1)
    CONDITION Pending WHEN status-code = "P"
    CONDITION Paid    WHEN status-code = "X"
    CONDITION Overdue WHEN status-code = "O"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The business rule lives next to the data it describes, in words. Six months from now, nobody has to decode what &lt;code&gt;status-code = "X"&lt;/code&gt; means scattered across the codebase. It says &lt;code&gt;Paid&lt;/code&gt; right where you read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that are firmly in this century
&lt;/h2&gt;

&lt;p&gt;This is where Kobol stops being a nostalgia trip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It runs on the JVM. No mainframe required.&lt;/strong&gt; Kobol compiles to JVM bytecode. It runs on your laptop, in a container, in CI, on whatever boring Linux box you already have. There is no special hardware, no emulator, no licensed runtime. If you can run Java, you can run Kobol.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Java interop.&lt;/strong&gt; You can &lt;code&gt;IMPORT&lt;/code&gt; and call Java directly, so the entire JVM ecosystem is available. You're not on an island.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;  &lt;span class="no"&gt;IMPORT&lt;/span&gt; &lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;LocalDate&lt;/span&gt;
  &lt;span class="no"&gt;IMPORT&lt;/span&gt; &lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DateTimeFormatter&lt;/span&gt; &lt;span class="no"&gt;AS&lt;/span&gt; &lt;span class="nc"&gt;DateFmt&lt;/span&gt;

  &lt;span class="no"&gt;PROCEDURE&lt;/span&gt; &lt;span class="nl"&gt;Main:&lt;/span&gt;
    &lt;span class="no"&gt;CALL&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt; &lt;span class="no"&gt;GIVING&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;
    &lt;span class="no"&gt;CALL&lt;/span&gt; &lt;span class="nc"&gt;DateFmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ISO_LOCAL_DATE&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt; &lt;span class="no"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="no"&gt;GIVING&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact decimal money, built in.&lt;/strong&gt; &lt;code&gt;MONEY(12.2)&lt;/code&gt; is real fixed-point decimal, not a float you cross your fingers over. Finance code shouldn't have to fight the language to add two numbers correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern data work.&lt;/strong&gt; Filtering, sorting, and taking the top N reads like a sentence:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  COMPUTE top5 = customer-list FILTER WHERE active SORT BY balance DESCENDING TAKE 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency without the ceremony&lt;/strong&gt;, structured error handling with &lt;code&gt;TRY / ON&lt;/code&gt;, records, lists, pipelines. The conveniences you'd expect from a language designed now.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who I'm hoping finds this
&lt;/h2&gt;

&lt;p&gt;Honestly? The next generation.&lt;/p&gt;

&lt;p&gt;There's a strange situation in our industry: an enormous amount of critical software is written in a style most new developers have never been taught and have been quietly told to avoid. The people who maintain it are retiring. The work isn't going anywhere.&lt;/p&gt;

&lt;p&gt;I'm not asking anyone to go learn 1959. I'm hoping a few curious people — students, early-career devs, anyone who's ever been burned by code they couldn't read — pick up Kobol, feel how natural readable code can be, and take that instinct with them everywhere, whatever language they write in next.&lt;/p&gt;

&lt;p&gt;And if you're coming &lt;em&gt;from&lt;/em&gt; COBOL: existing COBOL programs don't run on Kobol as-is. They have to be refitted and rewritten. I won't pretend otherwise. But because Kobol keeps the familiar English-like, readable shape, moving from COBOL to Kobol is a far gentler journey than porting a system into a syntax that looks nothing like what your team has read for decades. The mental model carries over; that's worth a lot on a migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  A genuine thank-you
&lt;/h2&gt;

&lt;p&gt;Credit where it belongs: Grace Hopper, Jean Sammet, and the CODASYL committee decided, in 1959, that code should be something humans can read. Kobol carries that idea forward into a world they'd recognize and a runtime they'd never have imagined. The foundational insight is theirs; building a modern language on top of it is the work I'm proud to have done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Kobol is open source and out now. Pick whichever line matches your machine. They all leave you with a working &lt;code&gt;kobol&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS (Apple Silicon) — Homebrew&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap kobol-lang/tap
brew &lt;span class="nb"&gt;install &lt;/span&gt;kobol
kobol &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Linux — native binary, no JVM needed&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# pick your arch: linux-x86_64 or linux-aarch64&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://github.com/kobol-lang/kobol/releases/latest/download/kobol-linux-x86_64.tar.gz | &lt;span class="nb"&gt;tar &lt;/span&gt;xz
&lt;span class="nb"&gt;sudo mv &lt;/span&gt;kobol /usr/local/bin/
kobol &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows — Chocolatey&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;choco&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;kobol&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;kobol&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Any machine with a JVM 21+ (Intel Macs included) — the fat JAR&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# grab the latest kobolc-&amp;lt;version&amp;gt;.jar from the releases page:&lt;/span&gt;
&lt;span class="c"&gt;#   https://github.com/kobol-lang/kobol/releases/latest&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; https://github.com/kobol-lang/kobol/releases/download/v0.1.0/kobolc-0.1.0.jar
java &lt;span class="nt"&gt;-jar&lt;/span&gt; kobolc-0.1.0.jar &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# handy alias:&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;kobol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"java -jar &lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/kobolc-0.1.0.jar"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Docker&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull ghcr.io/kobol-lang/kobol:latest
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; ghcr.io/kobol-lang/kobol:latest &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌐 Site: &lt;strong&gt;&lt;a href="https://kobol-lang.org/" rel="noopener noreferrer"&gt;kobol-lang.org&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;💻 Source: &lt;strong&gt;&lt;a href="https://github.com/kobol-lang/kobol" rel="noopener noreferrer"&gt;github.com/kobol-lang/kobol&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;📖 Language spec: &lt;strong&gt;&lt;a href="https://github.com/kobol-lang/kobol/blob/main/docs/LANGUAGE_SPEC.md" rel="noopener noreferrer"&gt;LANGUAGE_SPEC.md&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build something with it, or just have opinions, I'd love to hear them. Write code your future self can read at 2 a.m.&lt;/p&gt;

&lt;p&gt;— &lt;em&gt;the person who got tired of decoding their own cleverness&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cobol</category>
      <category>kobol</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>We Keep Building Bigger Walls Around the Wrong Thing</title>
      <dc:creator>V G P</dc:creator>
      <pubDate>Fri, 15 May 2026 22:34:14 +0000</pubDate>
      <link>https://dev.to/mrtinkz/we-keep-building-bigger-walls-around-the-wrong-thing-545b</link>
      <guid>https://dev.to/mrtinkz/we-keep-building-bigger-walls-around-the-wrong-thing-545b</guid>
      <description>&lt;p&gt;There's a species of tree in dense forests that grows toward whatever gap in the canopy lets light through. It doesn't ask permission. It doesn't register with the other trees. It just moves toward what it needs, and the forest works because of that — not in spite of it.&lt;/p&gt;

&lt;p&gt;We don't build software that way anymore.&lt;/p&gt;




&lt;p&gt;At some point we decided that before a user can do anything — read a note, save a preference, pick up where they left off — they need to announce themselves to a server, wait for a response, get issued a token, and carry that token everywhere like a passport in a country that checks papers at every door.&lt;/p&gt;

&lt;p&gt;And when that system gets too heavy, we don't question the system. We add another layer to it.&lt;/p&gt;

&lt;p&gt;There are entire job functions dedicated to managing this. Entire platforms — some charging by the login — whose whole purpose is to sit between your user and your app and say: &lt;em&gt;prove yourself first&lt;/em&gt;. The infrastructure behind a simple "remember my theme preference" can span multiple availability zones, three third-party vendors, and a compliance audit.&lt;/p&gt;

&lt;p&gt;Nobody stops to ask whether this was the only way.&lt;/p&gt;




&lt;p&gt;I'm not arguing against security. I'm asking about proportion.&lt;/p&gt;

&lt;p&gt;A river doesn't move water by building a bigger pump. It follows the path of least resistance, deposits what it carries at the delta, and keeps moving. The water arrives. Nothing registers. Nothing authenticates. The delta doesn't care where the water came from — it cares what the water does when it gets there.&lt;/p&gt;

&lt;p&gt;The session token model is the opposite of this. It says: water cannot flow until we verify it's water, issue it a credential, log that it arrived, and stand ready to revoke its water-ness at any time. Then we wonder why everything feels sluggish.&lt;/p&gt;




&lt;p&gt;What I've been working on with &lt;a href="https://www.npmjs.com/package/@mrtinkz/tessera" rel="noopener noreferrer"&gt;tessera&lt;/a&gt; starts from a different assumption.&lt;/p&gt;

&lt;p&gt;The user already has the device. The device already has the browser. The browser already has a crypto engine sitting idle — AES-256-GCM, PBKDF2, Web Crypto — powerful enough to handle what most apps actually need to protect. The passcode stays local. The key never leaves the device. The server never enters the picture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Tessera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferences&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No round-trip. No token. No registration. The user's data is encrypted with their own passcode, in their own browser, and only they can read it.&lt;/p&gt;

&lt;p&gt;That's not a compromise. For a significant class of problems, that's strictly better — because there's no central store to breach, no credential database to leak, no session to hijack in transit.&lt;/p&gt;




&lt;p&gt;There's a concept in structural design called the minimum viable structure — the least amount of material you can use while still holding the load. Bridges built this way are elegant. They carry weight precisely because they don't carry anything extra. Every beam earns its place.&lt;/p&gt;

&lt;p&gt;Most auth infrastructure fails this test badly. It carries weight that exists to justify itself: the logging layer that feeds the dashboard nobody checks, the token refresh cycle that prevents a session timeout nobody would have noticed, the password complexity rules enforced by a system that hashes the password anyway.&lt;/p&gt;

&lt;p&gt;tessera tries to be the minimum viable structure for what it actually does. Encrypt the data. Derive the key locally. Let the value expire when it should. Notice when something looks wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;one_time_code&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;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxReads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&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;That's it. The code is gone after one read. No server involved in that transaction. No revocation request. It just ceases to exist.&lt;/p&gt;




&lt;p&gt;The immune system doesn't keep a registry of every pathogen it's ever seen stored on a central server somewhere. It carries the memory in the cells themselves — distributed, local, present at the point of contact. When something unfamiliar shows up, the response comes from within, not from a round-trip to headquarters.&lt;/p&gt;

&lt;p&gt;tessera does something similar with honey keys — decoys planted inside storage that look identical to real entries. Real code never touches them. Anything enumerating storage and guessing will.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;honey-hit&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;event&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="c1"&gt;// something is probing your storage&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detection lives where the data lives. No external service needed.&lt;/p&gt;




&lt;p&gt;I'm not trying to kill authentication. There are things that genuinely require a server, a verified identity, a shared secret negotiated between two parties. I'm not pretending otherwise.&lt;/p&gt;

&lt;p&gt;But I think we've spent so long inside one way of thinking about identity and access that we've stopped noticing the assumptions embedded in it. That every interaction needs a server to bless it. That local state is inherently less trustworthy than remote state. That the right response to a security problem is always more infrastructure.&lt;/p&gt;

&lt;p&gt;Some problems don't need a bigger wall. They need a different shape entirely.&lt;/p&gt;




&lt;p&gt;tessera is small. It's a start. But it's built on the premise that the device in your user's hand is already powerful enough to protect their data — if you give it the right tools and get out of the way.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @mrtinkz/tessera
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/mrtinkz/tessera" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — I'd genuinely like to know where you think this line of thinking breaks down.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>@mrtinkz/tessera v0.1.2 — I Wasn't Done Yet</title>
      <dc:creator>V G P</dc:creator>
      <pubDate>Fri, 15 May 2026 22:19:49 +0000</pubDate>
      <link>https://dev.to/mrtinkz/tessera-v011-i-wasnt-done-yet-47he</link>
      <guid>https://dev.to/mrtinkz/tessera-v011-i-wasnt-done-yet-47he</guid>
      <description>&lt;p&gt;After shipping v0.1.0 I did what most developers do after a release — I opened my own app and started poking around.&lt;/p&gt;

&lt;p&gt;The values were ciphertext. Good. But the keys were sitting right there in plain English. &lt;code&gt;auth_state&lt;/code&gt;. &lt;code&gt;cart_items&lt;/code&gt;. &lt;code&gt;pending_payment&lt;/code&gt;. Anyone who opened DevTools knew exactly what I was keeping track of, even if they couldn't read the contents. That shouldn't have bothered me as much as it did. But I couldn't let it go.&lt;/p&gt;

&lt;p&gt;So I kept going.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your keys now mean nothing to anyone but you
&lt;/h2&gt;

&lt;p&gt;tessera now runs every key name through HMAC-SHA-256 before it touches storage. What you call &lt;code&gt;cart_items&lt;/code&gt;, tessera stores as &lt;code&gt;t_3a9f7c2e&lt;/code&gt;. Close DevTools, reopen it, and all you see is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t_3a9f7c2e  →  &amp;lt;ciphertext&amp;gt;
t_b2d4f110  →  &amp;lt;ciphertext&amp;gt;
t_03e8a5cc  →  &amp;lt;ciphertext&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mapping only exists in memory, derived from your passcode. Lock the vault — it's gone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Some of those entries are fake
&lt;/h2&gt;

&lt;p&gt;Here's the thing I'm most pleased with: not all of those entries are real. tessera automatically plants &lt;strong&gt;honey keys&lt;/strong&gt; — decoys that look exactly like real values. Same key format, same ciphertext format. Completely indistinguishable.&lt;/p&gt;

&lt;p&gt;Real code never touches them. Only something enumerating your storage and guessing would. That's the tripwire.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;honey-hit&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;event&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="c1"&gt;// something is probing your storage&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Values that delete themselves
&lt;/h2&gt;

&lt;p&gt;Some data shouldn't outlive its purpose. A one-time code. A recovery token. A payment session. v0.1.2 lets values carry their own expiry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;one_time_code&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;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// gone after 30 seconds&lt;/span&gt;
  &lt;span class="na"&gt;maxReads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// gone after first read&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no background timer running. The check happens at read time — the moment something requests the value, tessera looks at the write timestamp and acts. If it's expired, it wipes before returning anything. A timer can be cleared by an attacker. A check on read cannot.&lt;/p&gt;

&lt;p&gt;Don't want to configure every key manually? Sensitivity presets have you covered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recovery_code&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;sensitivity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// 5 minute TTL, 3 max reads, wiped at first sign of trouble&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  It notices things that shouldn't be happening
&lt;/h2&gt;

&lt;p&gt;The last thing I added was a suspicion engine. If reads start coming in faster than any human could trigger them, tessera notices. If an HMAC check fails on a read — meaning the value was touched outside the API — tessera notices that too.&lt;/p&gt;

&lt;p&gt;You decide what happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Tessera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;123456&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;suspicion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;callsPerSecond&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="na"&gt;onSuspicion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lock&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lock&lt;/code&gt;, &lt;code&gt;wipe&lt;/code&gt;, or &lt;code&gt;throw&lt;/code&gt;. tessera just makes sure &lt;em&gt;something&lt;/em&gt; happens.&lt;/p&gt;




&lt;p&gt;The encryption in v0.1.0 was the obvious part. v0.1.1 is all the stuff I couldn't stop thinking about after — the layers that make it hard to learn anything useful from your storage even when someone already has full read access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @mrtinkz/tessera
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/mrtinkz/tessera" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — feedback always welcome.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>I Built a Zero-Dependency Browser Storage Encryption Library — Here's Why</title>
      <dc:creator>V G P</dc:creator>
      <pubDate>Thu, 14 May 2026 05:21:55 +0000</pubDate>
      <link>https://dev.to/mrtinkz/i-built-a-zero-dependency-browser-storage-encryption-library-heres-why-444l</link>
      <guid>https://dev.to/mrtinkz/i-built-a-zero-dependency-browser-storage-encryption-library-heres-why-444l</guid>
      <description>&lt;p&gt;A few months ago I found myself auditing a side project and noticed something uncomfortable: I was storing sensitive user preferences, cart data, and session tokens in &lt;code&gt;localStorage&lt;/code&gt; — completely in plaintext. Anyone with DevTools open could read it in two seconds.&lt;/p&gt;

&lt;p&gt;The obvious fix is "just encrypt it." But when I went looking for a library that actually did this well, I kept running into the same problems: heavy dependencies, weak key derivation, or APIs that felt bolted on as an afterthought. So I built &lt;a href="https://www.npmjs.com/package/@mrtinkz/tessera" rel="noopener noreferrer"&gt;tessera&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What tessera does
&lt;/h2&gt;

&lt;p&gt;One passcode. All your browser storage — &lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;sessionStorage&lt;/code&gt;, &lt;code&gt;IndexedDB&lt;/code&gt;, and &lt;code&gt;cookies&lt;/code&gt; — encrypted with AES-256-GCM. The key is derived from PBKDF2-SHA-256 at ≥ 310,000 iterations (the OWASP 2024 minimum), and it never leaves the Web Crypto engine as raw bytes.&lt;/p&gt;

&lt;p&gt;The API is a drop-in replacement for the storage APIs you already use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Tessera&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mrtinkz/tessera&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Tessera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cartData&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;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// plaintext back&lt;/span&gt;
&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// zeroes the in-memory key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No server round-trips. No cloud keys. No dependencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  The threat model I was actually designing against
&lt;/h2&gt;

&lt;p&gt;Most encryption libraries stop at "we encrypt the data." tessera is built against the &lt;a href="https://owasp.org/www-community/threats/" rel="noopener noreferrer"&gt;OWASP browser storage threat model&lt;/a&gt;, so let me be specific about what it protects and what it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it protects against:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Passive observer in DevTools&lt;/strong&gt; — storage values are ciphertext. Useless without the key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XSS reading storage&lt;/strong&gt; — same deal. The attacker gets ciphertext.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline brute force&lt;/strong&gt; — PBKDF2 at 310k iterations costs roughly 1 second per attempt on modern hardware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key exfiltration via heap dump&lt;/strong&gt; — &lt;code&gt;extractable: false&lt;/code&gt; means the raw key bytes never exist in JavaScript memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-device brute force&lt;/strong&gt; — configurable lockout: wipe all storage, apply exponential backoff, or throw immediately after N failed attempts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't protect against:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;tessera protects your data at rest — when the vault is locked, everything in storage is ciphertext. Unlocking doesn't decrypt everything at once; it just derives the key and holds it in memory. Individual values only decrypt on demand when you call &lt;code&gt;getItem&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The one scenario where this breaks down is if your page already has an XSS vulnerability. An attacker running code on your page has the same access you do — they can call &lt;code&gt;vault.local.getItem()&lt;/code&gt; while the vault is unlocked and get plaintext back, one value at a time. They can't steal the raw key bytes (&lt;code&gt;extractable: false&lt;/code&gt; blocks that), but they don't need to. Fix XSS first; tessera handles the rest. The &lt;a href="https://github.com/mrtinkz/tessera/blob/main/docs/threat-model.md" rel="noopener noreferrer"&gt;threat model docs&lt;/a&gt; go deeper on this.&lt;/p&gt;




&lt;h2&gt;
  
  
  The crypto internals
&lt;/h2&gt;

&lt;p&gt;Each stored value gets its own salt and IV. The stored format is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;salt(16) ‖ iv(12) ‖ ciphertext ‖ tag(16)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The vault salt lives in &lt;code&gt;localStorage&lt;/code&gt; so the same passcode re-derives the same key across sessions — you unlock once per session, not once per page load.&lt;/p&gt;

&lt;p&gt;Key derivation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PBKDF2(passcode, vaultSalt, 310_000 iterations, SHA-256) → AES-256-GCM key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;CryptoKey&lt;/code&gt; is created with &lt;code&gt;extractable: false&lt;/code&gt;. The Web Crypto engine holds the key material; your JavaScript never sees the raw bytes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The PIN pad problem
&lt;/h2&gt;

&lt;p&gt;I wanted to mitigate keyloggers and click-sequence recording. The naive approach — an HTML grid of buttons with digit labels — fails because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keyloggers read &lt;code&gt;keydown&lt;/code&gt; events&lt;/li&gt;
&lt;li&gt;Click-sequence recording reads which DOM element was clicked&lt;/li&gt;
&lt;li&gt;The digit labels on buttons reveal the sequence&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;tessera ships a Canvas-based PIN pad. Digit positions are randomised on every render. No DOM element carries a digit value. A click recorder sees coordinates, not digits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;renderPinPad&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mrtinkz/tessera&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;renderPinPad&lt;/span&gt;&lt;span class="p"&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pin&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;onUnlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;passcode&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;vault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Tessera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;passcode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;randomize&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;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&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;You can style it with CSS custom properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.tessera-pin-pad&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--tessera-pad-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1a1a2e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tessera-btn-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#16213e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tessera-btn-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e2e8f0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tessera-btn-hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0f3460&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tessera-btn-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;64px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--tessera-indicator-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4ade80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Framework support
&lt;/h2&gt;

&lt;p&gt;tessera ships ESM, CJS, and IIFE builds. There are native adapters for React, Vue 3, Svelte, and Angular so you get a hook/store/service rather than managing vault state yourself.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useTessera&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mrtinkz/tessera/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SecureApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLocked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTessera&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;idleTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="nx"&gt;_000&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;isLocked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PinPad&lt;/span&gt; &lt;span class="na"&gt;onUnlock&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;unlock&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Dashboard&lt;/span&gt; &lt;span class="na"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onLock&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;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;strong&gt;Vue 3:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useTessera&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mrtinkz/tessera/vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLocked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTessera&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;idleTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tesseraStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mrtinkz/tessera/svelte&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLocked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tesseraStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;idleTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="nx"&gt;_000&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;There's also an Angular &lt;code&gt;TesseraModule&lt;/code&gt; and &lt;code&gt;TesseraService&lt;/code&gt; if that's your stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  Idle timeout and cross-tab sync
&lt;/h2&gt;

&lt;p&gt;The vault auto-locks after a configurable idle period. When it locks, it broadcasts over &lt;code&gt;BroadcastChannel&lt;/code&gt; so every open tab locks simultaneously. No stale unlocked tabs sitting in the background.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Tessera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&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;idleTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// 15 minutes&lt;/span&gt;
  &lt;span class="na"&gt;lockoutAttempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lockoutAction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// nuclear option&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @mrtinkz/tessera
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CDN:&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/@mrtinkz/tessera/dist/index.global.global.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;Tessera&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TesseraLib&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;Tessera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;vault&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;vault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Browser support: Chrome/Edge 89+, Firefox 86+, Safari 15+. Also works in Deno, Bun, and Cloudflare Workers.&lt;/p&gt;




&lt;p&gt;I'd love feedback — especially from anyone who's thought hard about browser storage security. What's missing? What would you do differently? Drop it in the comments or open an issue on &lt;a href="https://github.com/mrtinkz/tessera" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Docker Desktop Won't Start After a BIOS Update? Check This First</title>
      <dc:creator>V G P</dc:creator>
      <pubDate>Tue, 12 May 2026 02:36:29 +0000</pubDate>
      <link>https://dev.to/mrtinkz/docker-desktop-wont-start-after-a-bios-update-check-this-first-45nj</link>
      <guid>https://dev.to/mrtinkz/docker-desktop-wont-start-after-a-bios-update-check-this-first-45nj</guid>
      <description>&lt;p&gt;I spent way too long troubleshooting this. Reinstalled Docker Desktop multiple times, wiped config folders, checked WSL, verified Hyper-V — nothing worked. Turns out the fix was two PowerShell commands.&lt;/p&gt;

&lt;p&gt;Here's what happened and how to fix it fast if you run into the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Situation
&lt;/h2&gt;

&lt;p&gt;My ThinkPad T480 got a BIOS and firmware update. After rebooting, Docker Desktop stopped launching. No error dialog, no crash message — it just silently died every time. The whale icon would never show up in the system tray.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;wsl --list&lt;/code&gt; showed no &lt;code&gt;docker-desktop&lt;/code&gt; distro. The logs had nothing useful — just Docker initializing and then abruptly stopping. Looked like a WSL problem, a virtualization problem, or a busted installation. It was none of those.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Actually Wrong
&lt;/h2&gt;

&lt;p&gt;The BIOS update disrupted Windows service configurations. The Docker Desktop backend service — &lt;code&gt;com.docker.service&lt;/code&gt; — got flipped to &lt;strong&gt;Manual&lt;/strong&gt; startup, so it was no longer running when Windows booted.&lt;/p&gt;

&lt;p&gt;Docker Desktop's UI process depends entirely on that backend service. If the service isn't running, the UI launches, finds nothing to connect to, and kills itself immediately. No warning, no useful error — just gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Open PowerShell as Administrator and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Start-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;com.docker.service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;com.docker.service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;Status: Running&lt;/code&gt;. Then lock it in so it survives future reboots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Set-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;com.docker.service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-StartupType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Automatic&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then launch Docker Desktop normally from the Start Menu (or via PowerShell):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Start-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Program Files\Docker\Docker\Docker Desktop.exe"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Whale icon appears, engine starts, everything works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Save This for Next Time
&lt;/h2&gt;

&lt;p&gt;If Docker Desktop ever silently dies on you again, run this before doing anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;com.docker.service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the status is &lt;code&gt;Stopped&lt;/code&gt;, just start it. You'll save yourself an hour of unnecessary reinstalls.&lt;/p&gt;

&lt;p&gt;And if the service is running but Docker still won't start, then check the actual log for a real error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="s2"&gt;\Docker\log\host\com.docker.backend.exe.log"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Tail&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error|fatal|panic|fail"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-CaseSensitive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Tested on ThinkPad T480, Windows 11, WSL2 with Debian. Hope this saves someone the hour I lost.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>windows</category>
      <category>wsl</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
