<?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: Shivam</title>
    <description>The latest articles on DEV Community by Shivam (@devxshivaa).</description>
    <link>https://dev.to/devxshivaa</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%2F3938915%2F9dbaa611-f864-4986-8fcf-d3e0f394be93.png</url>
      <title>DEV Community: Shivam</title>
      <link>https://dev.to/devxshivaa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devxshivaa"/>
    <language>en</language>
    <item>
      <title>Building Real-Time Group Chat with Redis Streams and SSE in Next.js</title>
      <dc:creator>Shivam</dc:creator>
      <pubDate>Mon, 18 May 2026 20:59:19 +0000</pubDate>
      <link>https://dev.to/devxshivaa/building-real-time-group-chat-with-redis-streams-and-sse-in-nextjs-42ei</link>
      <guid>https://dev.to/devxshivaa/building-real-time-group-chat-with-redis-streams-and-sse-in-nextjs-42ei</guid>
      <description>&lt;p&gt;One of the features we’re most proud of at &lt;strong&gt;&lt;a href="https://fledgr.online" rel="noopener noreferrer"&gt;Fledgr&lt;/a&gt;&lt;/strong&gt; is &lt;strong&gt;Circles&lt;/strong&gt; — real-time campus group chats designed to feel instant without relying on traditional WebSockets.&lt;/p&gt;

&lt;p&gt;Getting there was a lot less straightforward than we expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why We Didn’t Use WebSockets
&lt;/h2&gt;

&lt;p&gt;At first, WebSockets seemed like the obvious choice.&lt;/p&gt;

&lt;p&gt;The problem is deployment constraints.&lt;/p&gt;

&lt;p&gt;Since Fledgr runs on Vercel, persistent WebSocket connections inside serverless functions aren’t really an option. You either introduce a separate socket infrastructure or work within the platform’s limits.&lt;/p&gt;

&lt;p&gt;We chose the second route.&lt;/p&gt;

&lt;p&gt;Instead of WebSockets, we built the real-time layer using &lt;strong&gt;Server-Sent Events (SSE)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;SSE is essentially a long-lived HTTP response where the server continuously streams updates while the client listens using the browser’s native &lt;code&gt;EventSource&lt;/code&gt; API.&lt;/p&gt;

&lt;p&gt;It’s unidirectional, which works perfectly for chat message delivery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Messages are streamed from server → client&lt;/li&gt;
&lt;li&gt;Sending messages still happens through normal &lt;code&gt;POST&lt;/code&gt; requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simple architecture. Fewer moving parts.&lt;/p&gt;

&lt;p&gt;At least in theory.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 60-Second Problem on Vercel
&lt;/h2&gt;

&lt;p&gt;The first issue showed up immediately.&lt;/p&gt;

&lt;p&gt;Vercel serverless functions time out after roughly 60 seconds.&lt;/p&gt;

&lt;p&gt;That means your SSE stream gets terminated every single minute.&lt;/p&gt;

&lt;p&gt;For a real-time chat system, that’s obviously a problem.&lt;/p&gt;

&lt;p&gt;The mistake would’ve been trying to “fight” the platform.&lt;/p&gt;

&lt;p&gt;Instead, we designed around the limitation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Designing Around Forced Disconnects
&lt;/h2&gt;

&lt;p&gt;Rather than waiting for Vercel to kill the connection unexpectedly, we close the SSE stream ourselves after ~55 seconds.&lt;/p&gt;

&lt;p&gt;The browser automatically reconnects using &lt;code&gt;EventSource&lt;/code&gt;, usually within a second or two.&lt;/p&gt;

&lt;p&gt;On reconnect, the client sends the last received message ID, and the server resumes streaming from that point forward.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;No visible interruptions&lt;/li&gt;
&lt;li&gt;No dropped messages&lt;/li&gt;
&lt;li&gt;No manual reconnect logic on the client&lt;/li&gt;
&lt;li&gt;Reconnection feels invisible to users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once we stopped treating reconnects as failures and started treating them as part of the architecture, the system became much simpler.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why We Chose Redis Streams Instead of Pub/Sub
&lt;/h2&gt;

&lt;p&gt;The backend streaming layer uses &lt;strong&gt;Redis Streams&lt;/strong&gt; with &lt;code&gt;XREAD&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We specifically avoided Redis Pub/Sub for one reason:&lt;/p&gt;

&lt;h3&gt;
  
  
  Reconnection safety.
&lt;/h3&gt;

&lt;p&gt;Pub/Sub is fire-and-forget.&lt;/p&gt;

&lt;p&gt;If a client disconnects for even a moment, any messages sent during that gap are permanently lost.&lt;/p&gt;

&lt;p&gt;That doesn’t work well with SSE reconnect behavior.&lt;/p&gt;

&lt;p&gt;Redis Streams solve this cleanly because clients can reconnect using the last processed message ID and continue reading from where they left off.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;```txt id="pw1jlwm"&lt;br&gt;
XREAD STREAMS circle:messages &lt;/p&gt;

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


That tiny detail ended up being one of the most important architectural decisions in the system.

---

## Stream Persistence Strategy

We don’t use Redis as permanent message storage.

Messages are still written to the main database.

Redis Streams only exist as the real-time transport layer.

Because of that, we aggressively cap stream lengths and let Redis automatically trim older entries.



```txt id="rk5q3sj"
MAXLEN ~ 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps memory usage predictable while still giving reconnecting clients enough history to catch up safely.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We’d Probably Do Differently
&lt;/h2&gt;

&lt;p&gt;If we weren’t on Vercel, we’d probably use WebSockets.&lt;/p&gt;

&lt;p&gt;The SSE + reconnect pattern works surprisingly well, but it introduces complexity that native socket infrastructure simply avoids.&lt;/p&gt;

&lt;p&gt;A lot of architectural decisions end up being shaped around the 60-second execution ceiling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reconnect handling&lt;/li&gt;
&lt;li&gt;Message replay&lt;/li&gt;
&lt;li&gt;Stream resumption&lt;/li&gt;
&lt;li&gt;Client synchronization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those aren’t impossible problems, but they’re problems you wouldn’t normally choose to have.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;That said, this setup has been extremely solid for us in production at &lt;strong&gt;&lt;a href="https://fledgr.online" rel="noopener noreferrer"&gt;Fledgr&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Circles now supports real-time campus conversations without running a dedicated socket server or maintaining separate infrastructure outside Vercel.&lt;/p&gt;

&lt;p&gt;If you’re building on Vercel and need real-time features without introducing WebSocket infrastructure, this pattern is absolutely viable.&lt;/p&gt;

&lt;p&gt;It’s not the most obvious solution — but under real-world constraints, it ended up being one of the most reliable ones we found.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>redis</category>
      <category>fledgr</category>
    </item>
    <item>
      <title>CSRF Protection That Actually Works in a Next.js 16 + React 19 App</title>
      <dc:creator>Shivam</dc:creator>
      <pubDate>Mon, 18 May 2026 20:57:45 +0000</pubDate>
      <link>https://dev.to/devxshivaa/csrf-protection-that-actually-works-in-a-nextjs-16-react-19-app-2kef</link>
      <guid>https://dev.to/devxshivaa/csrf-protection-that-actually-works-in-a-nextjs-16-react-19-app-2kef</guid>
      <description>&lt;p&gt;When we were building &lt;strong&gt;&lt;a href="https://fledgr.online" rel="noopener noreferrer"&gt;Fledgr&lt;/a&gt;&lt;/strong&gt;, CSRF protection was one of those areas where most advice felt either outdated or unnecessarily complicated.&lt;/p&gt;

&lt;p&gt;A lot of apps either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rely entirely on &lt;code&gt;SameSite&lt;/code&gt; cookies&lt;/li&gt;
&lt;li&gt;Add a heavy CSRF library without understanding what it’s doing&lt;/li&gt;
&lt;li&gt;Or skip protection altogether because “modern browsers already handle it”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those approaches felt great to us.&lt;/p&gt;

&lt;p&gt;So we ended up implementing a lightweight CSRF layer that’s now used across every state-mutating route in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why &lt;code&gt;SameSite&lt;/code&gt; Alone Isn’t Enough
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SameSite=Strict&lt;/code&gt; definitely helps.&lt;/p&gt;

&lt;p&gt;It prevents cookies from being attached to most cross-site requests, which blocks a large class of CSRF attacks automatically.&lt;/p&gt;

&lt;p&gt;But relying on it &lt;em&gt;alone&lt;/em&gt; has a few problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browser behavior can vary&lt;/li&gt;
&lt;li&gt;Redirect edge cases still exist&lt;/li&gt;
&lt;li&gt;Many frameworks default to &lt;code&gt;SameSite=Lax&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Lax&lt;/code&gt; still allows cookies on top-level navigations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So while &lt;code&gt;SameSite&lt;/code&gt; is an important security layer, we never treated it as the entire solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pattern We Use: Double-Submit Cookies
&lt;/h2&gt;

&lt;p&gt;The approach we settled on is the classic &lt;strong&gt;double-submit cookie pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The flow is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a CSRF token server-side&lt;/li&gt;
&lt;li&gt;Store it in a cookie&lt;/li&gt;
&lt;li&gt;Require every &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; request to include the same token in a custom header&lt;/li&gt;
&lt;li&gt;Verify both values match on the server
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;```ts id="vkwx9v"&lt;br&gt;
const headerToken = request.headers.get("x-csrf-token");&lt;br&gt;
const cookieToken = cookies().get("csrf_token")?.value;&lt;/p&gt;

&lt;p&gt;if (!headerToken || headerToken !== cookieToken) {&lt;br&gt;
  return Response.json(&lt;br&gt;
    { error: "Invalid request" },&lt;br&gt;
    { status: 403 }&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;

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


The important detail here is the security boundary.

A malicious site can sometimes trigger a request that includes cookies automatically.

What it *can’t* do is read the CSRF cookie and inject its value into a custom header.

That separation is what makes the pattern effective.

---

## Centralising Protection with `withApi()`

One thing we wanted to avoid was developers forgetting to add CSRF checks on new routes.

So instead of manually verifying tokens everywhere, we moved the logic into a shared wrapper we call `withApi()`.

Every state-mutating route passes through it automatically.

The wrapper handles:

* CSRF verification
* Authentication resolution
* Rate limiting
* Error formatting
* Request validation

That means new endpoints are protected by default rather than depending on memory or code review.

---

## Never Use Raw `fetch()` on the Client

Another lesson we learned pretty quickly:

If developers can call raw `fetch()` directly, CSRF headers eventually become inconsistent.

Someone forgets the header.
A helper gets bypassed.
A new feature ships without protection.

So we wrapped `fetch()` inside a single client utility that automatically:

* Reads the CSRF token from the cookie
* Injects the `x-csrf-token` header
* Applies shared defaults

That utility is now the only allowed way to make authenticated API requests inside the app.

One function.
Used everywhere.
No exceptions.

---

## Why the CSRF Cookie Isn’t `HttpOnly`

This usually surprises people.

The CSRF cookie needs to be readable by JavaScript so the client utility can inject it into request headers.

That means the cookie cannot be `HttpOnly`.

This is intentional.

The security model here isn’t about hiding the token from JavaScript — it’s about preventing *cross-origin attackers* from reading it.

We also set the CSRF cookie to:



```txt id="u7n3xy"
SameSite=Strict
Secure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives us another protection layer on top of header validation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Catches in Practice
&lt;/h2&gt;

&lt;p&gt;The interesting part is that this setup has already caught several mistakes during development.&lt;/p&gt;

&lt;p&gt;Missing headers.&lt;br&gt;
Incorrect client integrations.&lt;br&gt;
Routes accidentally bypassing wrappers.&lt;/p&gt;

&lt;p&gt;Without CSRF validation, some of those mistakes could have become production vulnerabilities.&lt;/p&gt;

&lt;p&gt;Instead, requests fail immediately and visibly during development.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;CSRF protection doesn’t need to be complicated, but it &lt;em&gt;does&lt;/em&gt; need to be deliberate.&lt;/p&gt;

&lt;p&gt;For us, the winning combination ended up being:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Double-submit cookies&lt;/li&gt;
&lt;li&gt;Strict cookie settings&lt;/li&gt;
&lt;li&gt;Centralised route wrappers&lt;/li&gt;
&lt;li&gt;A single API client utility&lt;/li&gt;
&lt;li&gt;Zero direct &lt;code&gt;fetch()&lt;/code&gt; usage for authenticated requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s lightweight, easy to reason about, and fits naturally into a modern &lt;strong&gt;Next.js 16 + React 19&lt;/strong&gt; stack.&lt;/p&gt;

&lt;p&gt;We’ve been running this approach across &lt;strong&gt;&lt;a href="https://fledgr.online" rel="noopener noreferrer"&gt;Fledgr&lt;/a&gt;&lt;/strong&gt; in production, and so far it’s proven both reliable and hard to misuse — which is exactly what good security infrastructure should be.&lt;/p&gt;

</description>
      <category>fledgr</category>
      <category>fledgrapp</category>
      <category>security</category>
    </item>
    <item>
      <title>How We Handle Encrypted Database Fields in a Next.js App</title>
      <dc:creator>Shivam</dc:creator>
      <pubDate>Mon, 18 May 2026 20:54:16 +0000</pubDate>
      <link>https://dev.to/devxshivaa/how-we-handle-encrypted-database-fields-in-a-nextjs-app-4g2e</link>
      <guid>https://dev.to/devxshivaa/how-we-handle-encrypted-database-fields-in-a-nextjs-app-4g2e</guid>
      <description>&lt;p&gt;When we started building &lt;strong&gt;&lt;a href="https://fledgr.online" rel="noopener noreferrer"&gt;Fledgr&lt;/a&gt;&lt;/strong&gt;, we knew pretty early that some data needed stronger protection than a locked-down database alone could provide.&lt;/p&gt;

&lt;p&gt;Things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UPI IDs&lt;/li&gt;
&lt;li&gt;Placement package figures&lt;/li&gt;
&lt;li&gt;Anonymous post content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a database dump ever leaked, we didn’t want sensitive information sitting there in readable form.&lt;/p&gt;

&lt;p&gt;That led us to implement &lt;strong&gt;field-level encryption&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;Instead of using one static encryption key everywhere, we derive a &lt;strong&gt;unique key per field&lt;/strong&gt; using HKDF.&lt;/p&gt;

&lt;p&gt;That means the encryption key for a user's UPI ID is cryptographically separate from the key used for placement data or anonymous content — even though they all originate from the same master secret.&lt;/p&gt;

&lt;p&gt;Every sensitive field gets its own context string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;db:users:upiId
db:placements:package
anon:posts:2025-01-15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nice part is that we never store these derived keys. They’re generated only when needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Encryption Flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;encrypt&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deriveKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&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;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aes-256-gcm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&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;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&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;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthTag&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AES-256-GCM&lt;/strong&gt; for authenticated encryption&lt;/li&gt;
&lt;li&gt;Random 12-byte IVs&lt;/li&gt;
&lt;li&gt;HKDF-derived contextual keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps encryption isolated across different parts of the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling Old Plaintext Data
&lt;/h2&gt;

&lt;p&gt;Rolling out encryption in production usually means dealing with legacy rows that were stored before encryption existed.&lt;/p&gt;

&lt;p&gt;We didn’t want migrations causing crashes or corrupting data, so every decrypt operation goes through a safe wrapper.&lt;/p&gt;

&lt;p&gt;If decryption fails, we simply return the original value instead of throwing an error.&lt;/p&gt;

&lt;p&gt;That allowed us to gradually migrate older data without downtime or breaking existing records.&lt;/p&gt;




&lt;h2&gt;
  
  
  Querying Encrypted Fields
&lt;/h2&gt;

&lt;p&gt;One challenge with encrypted data is querying it.&lt;/p&gt;

&lt;p&gt;You can’t directly run &lt;code&gt;WHERE email = ...&lt;/code&gt; against encrypted values because encryption output changes every time.&lt;/p&gt;

&lt;p&gt;To solve that, we use &lt;strong&gt;blind indexes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For searchable fields like college emails, we store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The encrypted value&lt;/li&gt;
&lt;li&gt;A separate HMAC-based hash column&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All lookup queries run against the hash instead of the encrypted field itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;email_hash = HMAC_SHA256(email)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual encrypted value is never used inside query conditions.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Actually Protects Against
&lt;/h2&gt;

&lt;p&gt;Field-level encryption is &lt;strong&gt;not&lt;/strong&gt; a replacement for proper secret management.&lt;/p&gt;

&lt;p&gt;If the master secret leaks, everything downstream is compromised too.&lt;/p&gt;

&lt;p&gt;But that wasn’t the primary threat model we were solving for.&lt;/p&gt;

&lt;p&gt;The goal was protecting against database-level exposure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Misconfigured backups&lt;/li&gt;
&lt;li&gt;Leaked database dumps&lt;/li&gt;
&lt;li&gt;Overprivileged read replicas&lt;/li&gt;
&lt;li&gt;Snapshot exposure&lt;/li&gt;
&lt;li&gt;Internal read-only access gone wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are realistic risks for modern applications, and field-level encryption helps reduce the blast radius significantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance Impact
&lt;/h2&gt;

&lt;p&gt;We’ve been running this setup in production at &lt;strong&gt;&lt;a href="https://fledgr.online" rel="noopener noreferrer"&gt;Fledgr&lt;/a&gt;&lt;/strong&gt; with almost no noticeable overhead.&lt;/p&gt;

&lt;p&gt;The derive-on-demand approach is lightweight enough that users never feel it, while still giving us much stronger isolation for sensitive data.&lt;/p&gt;

&lt;p&gt;For us, it ended up being one of those rare security improvements that added meaningful protection &lt;em&gt;without&lt;/em&gt; making development harder.&lt;/p&gt;

</description>
      <category>fledgr</category>
      <category>fledgrapp</category>
      <category>collegewritingplatform</category>
    </item>
  </channel>
</rss>
