<?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: Sven Herrmann</title>
    <description>The latest articles on DEV Community by Sven Herrmann (@thatscalaguy).</description>
    <link>https://dev.to/thatscalaguy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F279658%2F9619cd45-8be1-4fa7-8820-afc87dbba14f.png</url>
      <title>DEV Community: Sven Herrmann</title>
      <link>https://dev.to/thatscalaguy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thatscalaguy"/>
    <language>en</language>
    <item>
      <title>Encrypting PostgreSQL Columns in Scala with skunk-crypt</title>
      <dc:creator>Sven Herrmann</dc:creator>
      <pubDate>Mon, 08 Jun 2026 17:48:12 +0000</pubDate>
      <link>https://dev.to/thatscalaguy/encrypting-postgresql-columns-in-scala-with-skunk-crypt-21o3</link>
      <guid>https://dev.to/thatscalaguy/encrypting-postgresql-columns-in-scala-with-skunk-crypt-21o3</guid>
      <description>&lt;p&gt;Some columns shouldn't sit in your database as plain text. Email addresses, phone numbers, tax IDs, anything a regulator or a breach-disclosure law cares about. The usual answers are either heavy (Postgres &lt;code&gt;pgcrypto&lt;/code&gt; with key material living next to the data) or invasive (a bespoke encryption layer threaded through every query you write).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ThatScalaGuy/skunk-crypt" rel="noopener noreferrer"&gt;&lt;strong&gt;skunk-crypt&lt;/strong&gt;&lt;/a&gt; takes a smaller, sharper angle: encryption is just another &lt;a href="https://typelevel.org/skunk/" rel="noopener noreferrer"&gt;Skunk&lt;/a&gt; codec. You keep writing ordinary Skunk queries; you just swap &lt;code&gt;text&lt;/code&gt; for &lt;code&gt;crypt.text&lt;/code&gt;. Plain values in your application, AES-256-GCM ciphertext in the database, and the key never leaves your process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="s"&gt;"INSERT INTO users (email) VALUES ($text)"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;command&lt;/span&gt;

&lt;span class="c1"&gt;// after — same query, encrypted column&lt;/span&gt;
&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="s"&gt;"INSERT INTO users (email) VALUES (${crypt.text})"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;command&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole idea. The rest of this post is how to use it well: getting set up, the one design decision that actually matters (searchable vs. not), key rotation, and the sharp edges worth knowing before you ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: encryption as a codec
&lt;/h2&gt;

&lt;p&gt;A Skunk &lt;code&gt;Codec[A]&lt;/code&gt; knows two things — how to &lt;em&gt;encode&lt;/em&gt; an &lt;code&gt;A&lt;/code&gt; into the string Postgres stores, and how to &lt;em&gt;decode&lt;/em&gt; it back. skunk-crypt slots encryption into exactly that seam:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encode&lt;/strong&gt;: take your &lt;code&gt;String&lt;/code&gt;/&lt;code&gt;Int&lt;/code&gt;/&lt;code&gt;UUID&lt;/code&gt;/…, serialize it, encrypt the bytes, store the result as &lt;code&gt;TEXT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decode&lt;/strong&gt;: read the &lt;code&gt;TEXT&lt;/code&gt;, decrypt, parse back into your type.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two consequences fall out of this design, and they're worth internalizing up front:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every encrypted column is a &lt;code&gt;TEXT&lt;/code&gt; column&lt;/strong&gt;, regardless of its logical type. An encrypted &lt;code&gt;Int&lt;/code&gt; is stored as text, an encrypted &lt;code&gt;timestamptz&lt;/code&gt; is stored as text. The ciphertext is opaque to Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres never sees a plaintext value and never holds a key.&lt;/strong&gt; It stores and returns opaque strings. All crypto happens in your JVM. That also means Postgres can't do anything &lt;em&gt;clever&lt;/em&gt; with the value — no server-side range queries, no &lt;code&gt;LIKE&lt;/code&gt;, no arithmetic. More on that below, because it's the main constraint you design around.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;skunk-crypt is published for Scala 2.13 and 3. Add it (check the &lt;a href="https://central.sonatype.com/artifact/de.thatscalaguy/skunk-crypt_3" rel="noopener noreferrer"&gt;Maven Central badge&lt;/a&gt; for the current version):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="n"&gt;libraryDependencies&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"de.thatscalaguy"&lt;/span&gt; &lt;span class="o"&gt;%%&lt;/span&gt; &lt;span class="s"&gt;"skunk-crypt"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="s"&gt;"1.0.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skunk, Cats, and Cats Effect are declared &lt;code&gt;provided&lt;/code&gt;, so skunk-crypt reuses whatever versions are already on your classpath instead of dragging in its own. Make sure Skunk itself is present:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="n"&gt;libraryDependencies&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"org.tpolecat"&lt;/span&gt; &lt;span class="o"&gt;%%&lt;/span&gt; &lt;span class="s"&gt;"skunk-core"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="s"&gt;"1.0.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Generate a key
&lt;/h3&gt;

&lt;p&gt;Keys are raw AES keys, hex-encoded. 64 hex characters gives you AES-256 (32 and 48 are accepted too, for AES-128/192 — but there's no reason not to use 256):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat this string like any other top-tier secret. Keep it out of source control; load it from an environment variable, a secrets manager, or your config of choice. If you lose it, the encrypted columns are gone — that's the point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build a validated &lt;code&gt;CryptContext&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The key is wrapped in a &lt;code&gt;CryptContext&lt;/code&gt;. Construction is &lt;em&gt;validated&lt;/em&gt; and returns an &lt;code&gt;Either&lt;/code&gt;, so a malformed or wrong-length key fails immediately with a reason instead of detonating later inside a query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;de.thatscalaguy.skunkcrypt.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;

&lt;span class="n"&gt;given&lt;/span&gt; &lt;span class="nc"&gt;CryptContext&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt;
  &lt;span class="nc"&gt;CryptContext&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;keysFromHex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;env&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DB_ENC_KEY"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;fold&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="s"&gt;"Invalid encryption key: $reason"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;given&lt;/code&gt; matters: every &lt;code&gt;crypt&lt;/code&gt;/&lt;code&gt;cryptd&lt;/code&gt; codec takes the &lt;code&gt;CryptContext&lt;/code&gt; implicitly. Define it once where your wiring lives, and the codecs pick it up everywhere. No effect wrappers, no threading a key parameter through your repository layer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Fail loud on bad keys. The &lt;code&gt;.fold(sys.error, identity)&lt;/code&gt; above turns a bad key into an immediate startup crash. That's deliberate — you want "wrong key" to be a deploy-time failure, not a 2 a.m. decryption exception.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Your first encrypted round-trip
&lt;/h2&gt;

&lt;p&gt;Here's a complete, runnable program. The columns are declared &lt;code&gt;TEXT&lt;/code&gt; in Postgres even though &lt;code&gt;age&lt;/code&gt; is logically an &lt;code&gt;Int&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;age&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;cats.effect.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;skunk.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;skunk.implicits.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.typelevel.otel4s.trace.Tracer&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.typelevel.otel4s.metrics.Meter&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;de.thatscalaguy.skunkcrypt.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;

&lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Demo&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nv"&gt;IOApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Simple&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt;

  &lt;span class="kt"&gt;given&lt;/span&gt; &lt;span class="kt"&gt;Tracer&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;Tracer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Implicits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;noop&lt;/span&gt;
  &lt;span class="n"&gt;given&lt;/span&gt; &lt;span class="nc"&gt;Meter&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;  &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;Meter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Implicits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;noop&lt;/span&gt;

  &lt;span class="c1"&gt;// Generate with: openssl rand -hex 32&lt;/span&gt;
  &lt;span class="n"&gt;given&lt;/span&gt; &lt;span class="nc"&gt;CryptContext&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt;
    &lt;span class="nc"&gt;CryptContext&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;keysFromHex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;env&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DB_ENC_KEY"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;fold&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="s"&gt;"Invalid encryption key: $reason"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;session&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Resource&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;IO&lt;/span&gt;, &lt;span class="kt"&gt;Session&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt;
    &lt;span class="nc"&gt;Session&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Builder&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;withHost&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"localhost"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;withPort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;withUserAndPassword&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"postgres"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"postgres"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;withDatabase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"postgres"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;single&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Unit&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;use&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt;
      &lt;span class="k"&gt;_&lt;/span&gt; &lt;span class="k"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
             &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="s"&gt;"INSERT INTO users (email, age) VALUES (${cryptd.text}, ${crypt.int4})"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;command&lt;/span&gt;
           &lt;span class="o"&gt;)((&lt;/span&gt;&lt;span class="s"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
      &lt;span class="c1"&gt;// The database now holds ciphertext; we read it back as plain values:&lt;/span&gt;
      &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="s"&gt;"SELECT email, age FROM users"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cryptd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;text&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="nv"&gt;crypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;int4&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
              &lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;_&lt;/span&gt; &lt;span class="k"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nv"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// List((alice@example.com, 30))&lt;/span&gt;
    &lt;span class="nf"&gt;yield&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it, then peek at the raw row from &lt;code&gt;psql&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       email                          |        age
--------------------------------------+----------------------
 b3pQ7t1mZ2k9c0Aa.0.9sFh2Kx...==      | 7Yk2p0qL4mZ1aa.0.Qf...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you're looking at is the on-disk format: &lt;code&gt;base64(iv).keyIndex.base64(ciphertext)&lt;/code&gt;. The IV and the key index travel &lt;em&gt;with&lt;/em&gt; the value, which is what makes per-row IVs and key rotation work without any extra bookkeeping tables. Your application reads &lt;code&gt;alice@example.com&lt;/code&gt; and &lt;code&gt;30&lt;/code&gt;; the database only ever held the opaque strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one decision that matters: &lt;code&gt;crypt&lt;/code&gt; vs &lt;code&gt;cryptd&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;skunk-crypt gives you two codec objects with the &lt;em&gt;same&lt;/em&gt; set of codecs. Picking between them per column is the main design choice you make, so it's worth understanding precisely.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Same input → same ciphertext?&lt;/th&gt;
&lt;th&gt;Use it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;crypt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Non-deterministic&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; (random IV per write)&lt;/td&gt;
&lt;td&gt;The safe default — anything you don't search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cryptd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deterministic&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (synthetic IV)&lt;/td&gt;
&lt;td&gt;Columns you must match with &lt;code&gt;WHERE x = ?&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;crypt&lt;/code&gt; is the safe default.&lt;/strong&gt; Every write gets a fresh random IV, so encrypting &lt;code&gt;"alice@example.com"&lt;/code&gt; twice produces two completely different ciphertexts. An observer with full read access to the table can't even tell which rows share a value. The cost: you can &lt;em&gt;never&lt;/em&gt; query by that column — there's nothing to match against.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;cryptd&lt;/code&gt; is deterministic on purpose.&lt;/strong&gt; The same plaintext always encrypts to the same ciphertext, which is exactly what lets you do &lt;code&gt;WHERE email = ?&lt;/code&gt;. It achieves this without the classic fixed-IV footgun: instead of reusing one IV everywhere (which would leak the XOR of your plaintexts through GCM's keystream), it derives the IV from the plaintext itself via HMAC — a &lt;em&gt;synthetic IV&lt;/em&gt;, the same idea as AES-GCM-SIV. Distinct values still get distinct keystreams; equal values line up so they stay searchable.&lt;/p&gt;

&lt;p&gt;The trade-off is unavoidable and you should name it out loud when you choose &lt;code&gt;cryptd&lt;/code&gt;: &lt;strong&gt;a deterministic column reveals which rows share the same value.&lt;/strong&gt; An attacker who can read the table can see that 4,000 rows have the same &lt;code&gt;country&lt;/code&gt; ciphertext, or run a frequency analysis. That's fine for a high-cardinality identifier like an email you need to look up by; it's a poor choice for a low-cardinality field like &lt;code&gt;gender&lt;/code&gt; or &lt;code&gt;subscription_tier&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Rule of thumb: reach for &lt;code&gt;cryptd&lt;/code&gt; &lt;strong&gt;only&lt;/strong&gt; on columns you genuinely need to look up by exact value, and only when leaking equality is acceptable. Everything else gets &lt;code&gt;crypt&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A more realistic table
&lt;/h2&gt;

&lt;p&gt;Mixed columns is the normal case — some searchable, most not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- encrypted UUID, looked up by id  -&amp;gt; cryptd&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt;       &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- looked up at login               -&amp;gt; cryptd&lt;/span&gt;
  &lt;span class="n"&gt;full_name&lt;/span&gt;   &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- displayed, never queried         -&amp;gt; crypt&lt;/span&gt;
  &lt;span class="n"&gt;tax_id&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- sensitive, never queried         -&amp;gt; crypt&lt;/span&gt;
  &lt;span class="n"&gt;balance&lt;/span&gt;     &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- a BigDecimal, never queried      -&amp;gt; crypt&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="nb"&gt;TEXT&lt;/span&gt;    &lt;span class="c1"&gt;-- a timestamptz, never queried     -&amp;gt; crypt&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="s"&gt;"""INSERT INTO customers (id, email, full_name, tax_id, balance, created_at)
        VALUES (${cryptd.uuid}, ${cryptd.text}, ${crypt.text},
                ${crypt.text}, ${crypt.numeric}, ${crypt.timestamptz})"""&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;command&lt;/span&gt;

&lt;span class="c1"&gt;// pass the values in the same order:&lt;/span&gt;
&lt;span class="nv"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;insert&lt;/span&gt;&lt;span class="o"&gt;)(&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Alice Example"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"DE123456789"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"42.00"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the codecs read just like Skunk's built-in ones — &lt;code&gt;cryptd.uuid&lt;/code&gt;, &lt;code&gt;crypt.numeric&lt;/code&gt;, &lt;code&gt;crypt.timestamptz&lt;/code&gt;. You combine them with &lt;code&gt;~&lt;/code&gt; (or map to a case class) exactly as you always would. The encryption is invisible at the call site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching encrypted columns
&lt;/h2&gt;

&lt;p&gt;Because &lt;code&gt;email&lt;/code&gt; was written with &lt;code&gt;cryptd&lt;/code&gt;, the encrypted form of a given address is stable, so you can match it directly. The parameter is encrypted deterministically and compared against the stored ciphertext — Postgres does an ordinary string equality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;byEmail&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Query&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;, &lt;span class="kt"&gt;BigDecimal&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt;
  &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="s"&gt;"SELECT balance FROM customers WHERE email = ${cryptd.text}"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;crypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;numeric&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// you pass the plaintext; skunk-crypt encrypts it to the same ciphertext that's stored&lt;/span&gt;
&lt;span class="nv"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;byEmail&lt;/span&gt;&lt;span class="o"&gt;)(&lt;/span&gt;&lt;span class="s"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the payoff of deterministic mode, and it composes with a real performance win: &lt;strong&gt;the deterministic ciphertext is stored as exact &lt;code&gt;TEXT&lt;/code&gt;, so an ordinary B-tree index on the column makes the lookup fast.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;customers_email_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The index is over ciphertext, but since equal plaintext means equal ciphertext, an equality probe lands right on it. (Indexing a &lt;code&gt;crypt&lt;/code&gt; column is pointless — every row's ciphertext differs, so nothing ever matches.)&lt;/p&gt;

&lt;p&gt;What you &lt;strong&gt;cannot&lt;/strong&gt; do on any encrypted column:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LIKE&lt;/code&gt; / prefix / substring search — the ciphertext bears no resemblance to the plaintext.&lt;/li&gt;
&lt;li&gt;Range queries (&lt;code&gt;age &amp;gt; 18&lt;/code&gt;, &lt;code&gt;created_at BETWEEN …&lt;/code&gt;) or &lt;code&gt;ORDER BY&lt;/code&gt; — ordering is destroyed by encryption.&lt;/li&gt;
&lt;li&gt;Server-side arithmetic, &lt;code&gt;SUM&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt; on the value, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need those, the column probably can't be encrypted at rest this way — or you keep a separate, coarse, non-sensitive column to filter on (e.g. an unencrypted &lt;code&gt;age_band&lt;/code&gt;) and encrypt the precise value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Supported types
&lt;/h2&gt;

&lt;p&gt;Both &lt;code&gt;crypt&lt;/code&gt; and &lt;code&gt;cryptd&lt;/code&gt; expose the same codec set, so the search/no-search choice is orthogonal to the column's type:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Codec&lt;/th&gt;
&lt;th&gt;Scala type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;String&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;int2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Short&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;int4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Int&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;int8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Long&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;float4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Float&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;float8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Double&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Boolean&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uuid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;java.util.UUID&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;numeric&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BigDecimal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;java.time.LocalDate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;java.time.LocalDateTime&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timestamptz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;java.time.OffsetDateTime&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Key rotation
&lt;/h2&gt;

&lt;p&gt;Security policies (and auditors) eventually ask you to rotate keys. skunk-crypt builds this in: &lt;code&gt;keysFromHex&lt;/code&gt; accepts &lt;strong&gt;several&lt;/strong&gt; keys. Encryption always uses the &lt;strong&gt;last&lt;/strong&gt; one, and the index of the key used is embedded in each stored value — so older keys keep decrypting the rows they originally encrypted.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="c1"&gt;// new key encrypts; both keys still decrypt&lt;/span&gt;
&lt;span class="n"&gt;given&lt;/span&gt; &lt;span class="nc"&gt;CryptContext&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt;
  &lt;span class="nv"&gt;CryptContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;keysFromHex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oldKeyHex&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newKeyHex&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;fold&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;error&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;identity&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;Append the new key. New writes use it (key index &lt;code&gt;1&lt;/code&gt;); existing rows still carry index &lt;code&gt;0&lt;/code&gt; and decrypt with the old key.&lt;/li&gt;
&lt;li&gt;Over time — lazily, or in a batch job — re-encrypt old rows by reading and writing them back. Each rewrite re-encrypts with the newest key.&lt;/li&gt;
&lt;li&gt;Once every row is on the new key, you &lt;em&gt;could&lt;/em&gt; drop the old one.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Only ever append keys. Never reorder or remove them.&lt;/strong&gt; The key index is stored inside each row. Reorder the list and every existing row now points at the wrong key; remove a key still in use and those rows become undecryptable. Append-only is the whole contract.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The rotation gotcha for &lt;code&gt;cryptd&lt;/code&gt; columns
&lt;/h3&gt;

&lt;p&gt;Here's the sharp edge that the happy-path docs gloss over, and it's worth stopping on. &lt;strong&gt;A &lt;code&gt;cryptd&lt;/code&gt; column's ciphertext depends on the encryption key&lt;/strong&gt;, because the synthetic IV is derived from the plaintext &lt;em&gt;using the last key&lt;/em&gt;. So the moment you append a rotation key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Old rows still &lt;strong&gt;decrypt&lt;/strong&gt; fine (their key index is embedded). Reading is unaffected.&lt;/li&gt;
&lt;li&gt;But a fresh &lt;code&gt;WHERE email = ${cryptd.text}&lt;/code&gt; lookup now encrypts the parameter with the &lt;em&gt;new&lt;/em&gt; key — producing a &lt;em&gt;different&lt;/em&gt; ciphertext than the one stored under the old key. &lt;strong&gt;The equality match silently misses the old rows.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: rotating a key doesn't just change new writes, it temporarily breaks search over deterministic columns that haven't been re-encrypted yet. The fix is to re-encrypt those columns promptly after rotation rather than lazily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Re-encrypt every customer's deterministic columns onto the newest key.&lt;/span&gt;
&lt;span class="c1"&gt;// Reading decrypts with whatever key the row used; writing re-encrypts with the latest.&lt;/span&gt;
&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;reencrypt&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Unit&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt;
  &lt;span class="nv"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;use&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt;
      &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="s"&gt;"SELECT id, email FROM customers"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cryptd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;uuid&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="nv"&gt;cryptd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;text&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;_&lt;/span&gt;    &lt;span class="k"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nv"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;traverse_&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                  &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="s"&gt;"UPDATE customers SET email = ${cryptd.text} WHERE id = ${cryptd.uuid}"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;command&lt;/span&gt;
                &lt;span class="o"&gt;)((&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
              &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;yield&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;crypt&lt;/code&gt; (non-deterministic) columns this never bites, because you never search them — lazy re-encryption is perfectly fine there. It's specifically the searchable columns that need an eager rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error handling
&lt;/h2&gt;

&lt;p&gt;Two failure modes are typed, and both surface through Skunk's normal codec path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad key — caught at construction.&lt;/strong&gt; &lt;code&gt;keysFromHex&lt;/code&gt; returns &lt;code&gt;Either[String, CryptContext]&lt;/code&gt;, with the reason (and the offending key's index, for multi-key lists). A bad key can't reach a query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad ciphertext — caught at decode.&lt;/strong&gt; Decryption raises a &lt;code&gt;CryptError&lt;/code&gt; (a &lt;code&gt;RuntimeException&lt;/code&gt; subtype, so it propagates like any decode failure):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MalformedCiphertext&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The stored value isn't &lt;code&gt;iv.keyIndex.data&lt;/code&gt; — e.g. legacy plaintext&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DecryptionFailure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wrong key, unknown key index, or a failed GCM authentication tag&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;MalformedCiphertext&lt;/code&gt; is the one you'll meet first in real life: it's what you get when an existing, unencrypted column still holds plaintext and you point an encrypted codec at it. That's your signal to run a one-time migration — read each row with the &lt;em&gt;plain&lt;/em&gt; Skunk codec, write it back with the encrypted codec — before switching reads over.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;DecryptionFailure&lt;/code&gt; from a &lt;em&gt;modified&lt;/em&gt; value is a feature, not a bug: AES-GCM is authenticated, so a tampered or truncated ciphertext fails to decrypt rather than quietly returning garbage. If someone edits a byte in the database, you find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it protects — and what it doesn't
&lt;/h2&gt;

&lt;p&gt;Be honest with your threat model. skunk-crypt encrypts column &lt;strong&gt;values&lt;/strong&gt;. It is strong against the specific scenario of &lt;em&gt;someone reading your database&lt;/em&gt; — a leaked backup, a stolen replica, an over-privileged analyst, a &lt;code&gt;SELECT&lt;/code&gt;-only breach. They get ciphertext and, without your key, nothing else.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hide column names, row counts, or access patterns.&lt;/li&gt;
&lt;li&gt;Protect data in your application's memory (it's plaintext there by design).&lt;/li&gt;
&lt;li&gt;Replace transport security (use TLS to Postgres) or at-rest disk encryption — those defend different layers.&lt;/li&gt;
&lt;li&gt;Conceal equality on &lt;code&gt;cryptd&lt;/code&gt; columns (covered above).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's one focused layer: confidentiality of sensitive values against database-level exposure. Stack it with the others; don't expect it to be all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing your integration
&lt;/h2&gt;

&lt;p&gt;Codec round-trips are pure, so you can unit-test encrypt-then-decrypt without a database at all. For the real thing — values actually flowing through Postgres — skunk-crypt's own suite uses &lt;a href="https://testcontainers.com/" rel="noopener noreferrer"&gt;Testcontainers&lt;/a&gt; to spin up a throwaway &lt;code&gt;postgres:16&lt;/code&gt;, which is a good pattern to copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"round-trips text and int columns through Postgres"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;withContainers&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;GenericContainer&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;container&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;getMappedPort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="py"&gt;use&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;for&lt;/span&gt;
        &lt;span class="k"&gt;_&lt;/span&gt;    &lt;span class="k"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                  &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="s"&gt;"INSERT INTO test (string, numbers) VALUES (${cryptd.text}, ${cryptd.int4})"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;command&lt;/span&gt;
                &lt;span class="o"&gt;)((&lt;/span&gt;&lt;span class="s"&gt;"Hello"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                  &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="s"&gt;"SELECT string, numbers FROM test"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cryptd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;text&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="nv"&gt;cryptd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;int4&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello"&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing worth asserting beyond a plain round-trip: that the &lt;em&gt;stored&lt;/em&gt; form really is ciphertext (read the column with a plain &lt;code&gt;text&lt;/code&gt; codec and check it doesn't equal the plaintext), and that a &lt;code&gt;cryptd&lt;/code&gt; write is stable across runs while a &lt;code&gt;crypt&lt;/code&gt; write isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist before you ship
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Encryption key loaded from a secret, &lt;strong&gt;never&lt;/strong&gt; committed. Startup fails loudly on a bad key.&lt;/li&gt;
&lt;li&gt;[ ] Every sensitive column is &lt;code&gt;TEXT&lt;/code&gt; in the schema.&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;cryptd&lt;/code&gt; only where you must search by exact value &lt;em&gt;and&lt;/em&gt; leaking equality is acceptable; &lt;code&gt;crypt&lt;/code&gt; everywhere else.&lt;/li&gt;
&lt;li&gt;[ ] B-tree index on each &lt;code&gt;cryptd&lt;/code&gt; column you look up by.&lt;/li&gt;
&lt;li&gt;[ ] A migration plan for any column that currently holds plaintext (watch for &lt;code&gt;MalformedCiphertext&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;[ ] A rotation runbook that &lt;strong&gt;appends&lt;/strong&gt; keys and &lt;strong&gt;eagerly re-encrypts&lt;/strong&gt; &lt;code&gt;cryptd&lt;/code&gt; columns.&lt;/li&gt;
&lt;li&gt;[ ] You've written down what this does and doesn't protect, so nobody assumes it's more than column-value confidentiality.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The thing I like about skunk-crypt is that it refuses to be a framework. It's a handful of codecs and one implicit context. You don't restructure your code around it — you swap &lt;code&gt;text&lt;/code&gt; for &lt;code&gt;crypt.text&lt;/code&gt;, decide per column whether you need to search it, and carry on writing ordinary Skunk. The hard parts (authenticated encryption, per-row IVs, embedded key indices for rotation) are handled; the parts you have to think about (deterministic-equality leakage, rotation + re-encryption, threat model) are the parts no library can decide for you anyway.&lt;/p&gt;

&lt;p&gt;Source, issues, and the full API reference:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/ThatScalaGuy/skunk-crypt" rel="noopener noreferrer"&gt;github.com/ThatScalaGuy/skunk-crypt&lt;/a&gt;&lt;/strong&gt; · &lt;a href="https://javadoc.io/doc/de.thatscalaguy/skunk-crypt_3" rel="noopener noreferrer"&gt;Scaladoc&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's Apache-2.0 licensed and contributions are welcome. If you put it into production, I'd genuinely like to hear how it goes — and if your project needs a hand with Scala, data, or the boring-but-critical security plumbing, &lt;a href="https://www.thatscalaguy.de" rel="noopener noreferrer"&gt;get in touch&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>scala</category>
      <category>postgres</category>
      <category>encryption</category>
      <category>security</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Sven Herrmann</dc:creator>
      <pubDate>Thu, 28 May 2026 18:37:55 +0000</pubDate>
      <link>https://dev.to/thatscalaguy/-1gca</link>
      <guid>https://dev.to/thatscalaguy/-1gca</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/thatscalaguy/gpgpujs-run-javascript-on-your-gpu-with-zero-shader-knowledge-569n" class="crayons-story__hidden-navigation-link"&gt;GPGPU.js: Run JavaScript on Your GPU With Zero Shader Knowledge&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/thatscalaguy" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F279658%2F9619cd45-8be1-4fa7-8820-afc87dbba14f.png" alt="thatscalaguy profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/thatscalaguy" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Sven Herrmann
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Sven Herrmann
                
              
              &lt;div id="story-author-preview-content-3747454" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/thatscalaguy" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F279658%2F9619cd45-8be1-4fa7-8820-afc87dbba14f.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Sven Herrmann&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/thatscalaguy/gpgpujs-run-javascript-on-your-gpu-with-zero-shader-knowledge-569n" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/thatscalaguy/gpgpujs-run-javascript-on-your-gpu-with-zero-shader-knowledge-569n" id="article-link-3747454"&gt;
          GPGPU.js: Run JavaScript on Your GPU With Zero Shader Knowledge
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webgpu"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webgpu&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/typescript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;typescript&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/performance"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;performance&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/thatscalaguy/gpgpujs-run-javascript-on-your-gpu-with-zero-shader-knowledge-569n" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/thatscalaguy/gpgpujs-run-javascript-on-your-gpu-with-zero-shader-knowledge-569n#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>javascript</category>
      <category>performance</category>
      <category>tooling</category>
      <category>webdev</category>
    </item>
    <item>
      <title>GPGPU.js: Run JavaScript on Your GPU With Zero Shader Knowledge</title>
      <dc:creator>Sven Herrmann</dc:creator>
      <pubDate>Mon, 25 May 2026 08:15:44 +0000</pubDate>
      <link>https://dev.to/thatscalaguy/gpgpujs-run-javascript-on-your-gpu-with-zero-shader-knowledge-569n</link>
      <guid>https://dev.to/thatscalaguy/gpgpujs-run-javascript-on-your-gpu-with-zero-shader-knowledge-569n</guid>
      <description>&lt;p&gt;JavaScript has a parallelism problem. The language is single-threaded by design, and while Web Workers help, they top out at a handful of cores. Meanwhile, the most powerful parallel processor in your machine — the GPU, with its thousands of cores — has been almost completely off-limits to web developers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API" rel="noopener noreferrer"&gt;WebGPU&lt;/a&gt; changes that. It exposes the GPU to the browser for general-purpose compute, not just graphics. The catch? Using it directly means writing WGSL shaders, managing buffers and bind groups, handling device initialization, and orchestrating async data transfers. That's a lot of boilerplate for what is conceptually just "run this function over an array, fast."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/ThatScalaGuy/GPGPU.js" rel="noopener noreferrer"&gt;GPGPU.js&lt;/a&gt;&lt;/strong&gt; removes that boilerplate. You write plain JavaScript; it runs on the GPU.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;gpu&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="s2"&gt;@thatscalaguy/gpgpu.js&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;doubled&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;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&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;// [2, 4, 6, 8]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No shaders. No buffers. No device setup. That &lt;code&gt;x =&amp;gt; x * 2&lt;/code&gt; arrow function is parsed and compiled to a WGSL compute shader, dispatched across the GPU, and the result comes back as a typed array.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎮 &lt;strong&gt;Try it right now in your browser&lt;/strong&gt; — no install needed: &lt;a href="https://thatscalaguy.github.io/GPGPU.js/" rel="noopener noreferrer"&gt;thatscalaguy.github.io/GPGPU.js&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The API in 30 seconds
&lt;/h2&gt;

&lt;p&gt;The whole point is that the surface area is tiny. Here's most of 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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;gpu&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="s2"&gt;@thatscalaguy/gpgpu.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Element-wise math (arrays or array + scalar)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&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="mi"&gt;2&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="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&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="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;   &lt;span class="c1"&gt;// [5, 7, 9]&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;multiply&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="mi"&gt;2&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="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// [10, 20, 30]&lt;/span&gt;

&lt;span class="c1"&gt;// Map with a JS arrow function — compiled to a GPU shader&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&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;// Reductions&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&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="mi"&gt;2&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="mi"&gt;4&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="c1"&gt;// 15&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&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="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;     &lt;span class="c1"&gt;// 9&lt;/span&gt;

&lt;span class="c1"&gt;// Sort (GPU bitonic sort) and prefix sum&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Matrix multiply (tiled, uses shared memory)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matmul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;matB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rowsA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colsA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colsB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is &lt;code&gt;async&lt;/code&gt; because the GPU works asynchronously — you &lt;code&gt;await&lt;/code&gt; the result and get a &lt;code&gt;Float32Array&lt;/code&gt; back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arrow functions become shaders
&lt;/h2&gt;

&lt;p&gt;The magic trick is the codegen. When you pass &lt;code&gt;x =&amp;gt; x * 2 + 1&lt;/code&gt;, GPGPU.js:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parses the function&lt;/strong&gt; and builds an intermediate representation of the expression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emits WGSL&lt;/strong&gt; from that IR — &lt;code&gt;x * 2 + 1&lt;/code&gt; becomes &lt;code&gt;output[idx] = (x * 2.0) + 1.0;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compiles and dispatches&lt;/strong&gt; the shader through WebGPU across thousands of cores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Returns&lt;/strong&gt; the result as a typed array.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A useful subset of JavaScript is supported inside expressions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Arithmetic: &lt;code&gt;+ - * / %&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Comparisons: &lt;code&gt;&amp;lt; &amp;gt; &amp;lt;= &amp;gt;= == !=&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ternary: &lt;code&gt;a &amp;gt; 0 ? a : -a&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Math functions: &lt;code&gt;Math.abs&lt;/code&gt;, &lt;code&gt;Math.sqrt&lt;/code&gt;, &lt;code&gt;Math.pow&lt;/code&gt;, &lt;code&gt;Math.min&lt;/code&gt;, &lt;code&gt;Math.max&lt;/code&gt;, &lt;code&gt;Math.floor&lt;/code&gt;, &lt;code&gt;Math.ceil&lt;/code&gt;, &lt;code&gt;Math.sin&lt;/code&gt;, &lt;code&gt;Math.cos&lt;/code&gt;, &lt;code&gt;Math.tan&lt;/code&gt;, &lt;code&gt;Math.exp&lt;/code&gt;, &lt;code&gt;Math.log&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're worried about minifiers mangling your arrow functions in production, you can pass a string expression instead — it compiles to exactly the same shader:&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;await&lt;/span&gt; &lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x * x + 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// minifier-safe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pipelines: keep data on the GPU
&lt;/h2&gt;

&lt;p&gt;The expensive part of GPU computing usually isn't the math — it's shuttling data back and forth across the PCIe bus between CPU and GPU. If you naively chain operations, you pay that round-trip every step:&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;// ❌ Three CPU↔GPU round-trips&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;r&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;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;r&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;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;total&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;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pipelines fix this. The data stays resident on the GPU between steps, and you only transfer at the start and end:&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;// ✅ One round-trip, data stays on GPU between steps&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&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;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&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;span class="nf"&gt;reduce&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;b&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;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For any non-trivial workload, this is the difference between "the GPU is slower than the CPU" and a real speedup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Escape hatch: write your own WGSL
&lt;/h2&gt;

&lt;p&gt;The high-level API covers a lot, but sometimes you need full control. The &lt;code&gt;createKernel&lt;/code&gt; API lets you write raw WGSL while still letting GPGPU.js handle device setup, buffer pooling, and dispatch:&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;kernel&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;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createKernel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;workgroupSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;shader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    @group(0) @binding(0) var&amp;lt;storage, read&amp;gt; input0: array&amp;lt;f32&amp;gt;;
    @group(0) @binding(1) var&amp;lt;storage, read_write&amp;gt; output: array&amp;lt;f32&amp;gt;;
    @compute @workgroup_size(64)
    fn main(@builtin(global_invocation_id) gid: vec3u) {
      let idx = gid.x;
      output[idx] = input0[idx] * input0[idx];
    }
  `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;f32&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;f32&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inputData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  It runs everywhere — even without WebGPU
&lt;/h2&gt;

&lt;p&gt;WebGPU support is good and growing, but it's not universal yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome 113+ / Edge 113+&lt;/li&gt;
&lt;li&gt;Firefox 141+ (Windows), 145+ (macOS)&lt;/li&gt;
&lt;li&gt;Safari 18+&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GPGPU.js doesn't make you choose. When WebGPU is unavailable, &lt;strong&gt;every operation transparently falls back to a CPU implementation&lt;/strong&gt;. Your code doesn't change; it just runs slower where there's no GPU and faster where there is. That makes it safe to ship in production today without feature-detection branches scattered through your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built for modern JavaScript projects
&lt;/h2&gt;

&lt;p&gt;A few things that make it pleasant to actually use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript-first&lt;/strong&gt; — full type safety, zero runtime dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tree-shakeable&lt;/strong&gt; — ships both ESM and CJS; import only what you use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No build step required&lt;/strong&gt; — works straight from npm.
&lt;/li&gt;
&lt;/ul&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; @thatscalaguy/gpgpu.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also create isolated instances instead of using the default singleton, and clean them up explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GPU&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="s2"&gt;@thatscalaguy/gpgpu.js&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;myGpu&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;GPU&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// ... use it ...&lt;/span&gt;
&lt;span class="nx"&gt;myGpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// release GPU resources when done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this is useful
&lt;/h2&gt;

&lt;p&gt;Anything that's "the same operation over a lot of data" is a candidate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Image and signal processing (per-pixel transforms, convolutions)&lt;/li&gt;
&lt;li&gt;Numerical simulations and physics&lt;/li&gt;
&lt;li&gt;Data transforms over large arrays&lt;/li&gt;
&lt;li&gt;Linear algebra — matrix multiplication is built in&lt;/li&gt;
&lt;li&gt;ML inference building blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your workload is small or branch-heavy, the CPU may still win — GPU shines when you have thousands to millions of independent elements. As always: measure.&lt;/p&gt;

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

&lt;p&gt;The fastest way to get a feel for it is the hosted playground — write an expression, hit run, see it execute on your actual GPU:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://thatscalaguy.github.io/GPGPU.js/" rel="noopener noreferrer"&gt;thatscalaguy.github.io/GPGPU.js&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the source, issues, and docs live on GitHub:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/ThatScalaGuy/GPGPU.js" rel="noopener noreferrer"&gt;github.com/ThatScalaGuy/GPGPU.js&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's MIT-licensed and contributions are welcome. If you build something with it, I'd love to hear about it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;GPGPU.js is open source under the MIT license.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webgpu</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
