<?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: Chizee</title>
    <description>The latest articles on DEV Community by Chizee (@chizee).</description>
    <link>https://dev.to/chizee</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4066137%2Fea432cc9-6883-4e71-afc0-40c7345c5ead.png</url>
      <title>DEV Community: Chizee</title>
      <link>https://dev.to/chizee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chizee"/>
    <language>en</language>
    <item>
      <title>I Replaced 8 Services With Postgres. Here's Where It Actually Breaks.</title>
      <dc:creator>Chizee</dc:creator>
      <pubDate>Sun, 18 Oct 2026 10:09:50 +0000</pubDate>
      <link>https://dev.to/chizee/i-replaced-8-services-with-postgres-heres-where-it-actually-breaks-ok8</link>
      <guid>https://dev.to/chizee/i-replaced-8-services-with-postgres-heres-where-it-actually-breaks-ok8</guid>
      <description>&lt;p&gt;Over the last few months I've written about replacing Redis, Elasticsearch, Pinecone, MongoDB, InfluxDB, Snowflake, a GIS stack, and a hand-built API layer — all with a single Postgres instance. Every one of those posts was true, tested, and worth doing for most teams.&lt;/p&gt;

&lt;p&gt;None of that means Postgres has no limits. It means most applications never actually reach them. Here's exactly where the line sits, because "it depends" isn't a real answer and I'd rather just tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this genuinely stops working
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Horizontal sharding is still hard
&lt;/h3&gt;

&lt;p&gt;Postgres scales vertically with real grace, but splitting one logical database across many physical nodes adds genuine operational complexity. Tools like Citus help — they don't make it free.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extreme ingest rates
&lt;/h3&gt;

&lt;p&gt;If you're ingesting millions of events per second, a single Postgres primary becomes your bottleneck before your application code does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sub-millisecond, massive-fanout caching
&lt;/h3&gt;

&lt;p&gt;Thousands of concurrent WebSocket connections, or read-after-write caching at extreme QPS, still wants a pure in-memory store. This is the one place on the list where the tool I told you to delete is genuinely the right one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connection limits
&lt;/h3&gt;

&lt;p&gt;Postgres handles a few thousand direct connections comfortably. Past that you need a pooler — PgBouncer or Supavisor — which is normal operations at scale, not a flaw to work around.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vacuum and bloat
&lt;/h3&gt;

&lt;p&gt;Heavy update/delete workloads need autovacuum tuned deliberately, or table bloat creeps in over time. This is routine Postgres maintenance, not optional homework.&lt;/p&gt;

&lt;h3&gt;
  
  
  One database, one blast radius
&lt;/h3&gt;

&lt;p&gt;Consolidating everything onto one instance means a single maintenance window or outage touches everything it powers. Plan replicas and backups accordingly — this is the actual cost of the simplicity, not a hidden one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest takeaway
&lt;/h2&gt;

&lt;p&gt;None of this means don't do it. It means know exactly where the line is, and cross it on purpose — not by accident, three months after your traffic graph quietly tripled.&lt;/p&gt;

&lt;p&gt;If you've read the previous eight posts in this series and are wondering "so when do I actually need to reach for something else" — this is that answer, in full, with nothing softened for the sake of a tidier narrative.&lt;/p&gt;




&lt;p&gt;This wraps an eight-part series on replacing common infrastructure with Postgres. All nine pieces — including the honest limits above, plus a 5-phase rollout plan for migrating without a 2am incident — are collected in &lt;strong&gt;Just Use Postgres&lt;/strong&gt;, a field manual with runnable SQL for every recipe.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Get the field manual&lt;/strong&gt; — launch price &lt;strong&gt;$14&lt;/strong&gt;: &lt;a href="https://payhip.com/omenabyte" rel="noopener noreferrer"&gt;https://payhip.com/omenabyte&lt;/a&gt;&lt;br&gt;
🐳 Or take the &lt;strong&gt;$24 bundle&lt;/strong&gt; with the full &lt;code&gt;docker-compose up&lt;/code&gt; starter repo — all 8 modules as tested, runnable migrations + seed data: &lt;a href="https://4693433176360.gumroad.com/" rel="noopener noreferrer"&gt;https://4693433176360.gumroad.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Read this on omenabyte.com → &lt;a href="https://omenabyte.com/blog/where-postgres-only-architecture-breaks" rel="noopener noreferrer"&gt;https://omenabyte.com/blog/where-postgres-only-architecture-breaks&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>architecture</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Let Your Database Generate the API and Guard the Door</title>
      <dc:creator>Chizee</dc:creator>
      <pubDate>Sun, 11 Oct 2026 10:09:16 +0000</pubDate>
      <link>https://dev.to/chizee/let-your-database-generate-the-api-and-guard-the-door-897</link>
      <guid>https://dev.to/chizee/let-your-database-generate-the-api-and-guard-the-door-897</guid>
      <description>&lt;p&gt;Most CRUD backends are the same boilerplate repeated per table: a route, a controller, a serializer, an authorization check. That's thousands of lines maintaining plumbing that doesn't encode a single real business rule.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PostgREST&lt;/code&gt; turns your schema directly into a documented REST API. &lt;code&gt;Row-Level Security&lt;/code&gt; moves authorization into the database itself, enforced on every query — including ones that bypass your application entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The policy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="n"&gt;ENABLE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"Users see only their own notes"&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth_user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'request.jwt.claim.sub'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgREST injects &lt;code&gt;request.jwt.claim.sub&lt;/code&gt; automatically from the caller's JWT on every request — you never set this yourself in application code.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;NULLIF(..., '')&lt;/code&gt; is there for a reason I only found by testing this directly. &lt;code&gt;current_setting(name, true)&lt;/code&gt; correctly returns &lt;code&gt;NULL&lt;/code&gt; the &lt;em&gt;first&lt;/em&gt; time a custom setting is unset in a session — but once it's been referenced even once (say, inside a transaction that later rolled back), Postgres leaves behind an empty-string placeholder for the rest of that session. From then on, &lt;code&gt;current_setting&lt;/code&gt; returns &lt;code&gt;''&lt;/code&gt;, not &lt;code&gt;NULL&lt;/code&gt;, and casting an empty string to &lt;code&gt;uuid&lt;/code&gt; throws an error. &lt;code&gt;NULLIF&lt;/code&gt; closes that gap for both cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bypass nobody warns you about
&lt;/h2&gt;

&lt;p&gt;Table owners and superusers bypass Row-Level Security &lt;strong&gt;by default&lt;/strong&gt; — policies simply don't apply to them. I confirmed this directly: as the table owner, a query against a two-row table with a strict policy in place still returned both rows. If you test your policy logged in as the role that created the table, it will always look unrestricted, whether or not the policy is actually correct.&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="k"&gt;FORCE&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;SECURITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This closes the gap if the policy needs to hold even for the owner. Either way, always test as the actual application role, not your own login.&lt;/p&gt;

&lt;h2&gt;
  
  
  What PostgREST auto-generates from this
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET  /notes   -&amp;gt;  rows already filtered by the policy above
POST /notes   -&amp;gt;  insert validated against the same policy set
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a column, the API reflects it immediately. No controller to update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this beats hand-rolling an API layer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No controller per table — the schema &lt;em&gt;is&lt;/em&gt; the API surface.&lt;/li&gt;
&lt;li&gt;Security lives in one place instead of scattered across every endpoint that happens to touch a table.&lt;/li&gt;
&lt;li&gt;The rule holds even if a client somehow calls the database directly — it's enforced below the application layer, not inside it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where a custom API still wins
&lt;/h2&gt;

&lt;p&gt;Complex multi-step workflows, third-party orchestration, or business logic that genuinely doesn't map onto table operations — payment flows, multi-system sagas, anything truly procedural. For straightforward CRUD, you're likely maintaining code that generates itself.&lt;/p&gt;




&lt;p&gt;This is one of eight infrastructure swaps in &lt;strong&gt;Just Use Postgres&lt;/strong&gt;, a 24-page field manual on replacing MongoDB, Redis, Elasticsearch, Pinecone, and more with the database you're probably already running. Every recipe in it — including this one — was run against a live Postgres instance before it went in the book.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Get the field manual&lt;/strong&gt; — launch price &lt;strong&gt;$14&lt;/strong&gt; instead of hand-writing a controller for every table: &lt;a href="https://payhip.com/omenabyte" rel="noopener noreferrer"&gt;https://payhip.com/omenabyte&lt;/a&gt;&lt;br&gt;
🐳 Or take the &lt;strong&gt;$24 bundle&lt;/strong&gt; with the full &lt;code&gt;docker-compose up&lt;/code&gt; starter repo — all 8 modules as tested, runnable migrations + seed data: &lt;a href="https://4693433176360.gumroad.com/" rel="noopener noreferrer"&gt;https://4693433176360.gumroad.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Read this on omenabyte.com → &lt;a href="https://omenabyte.com/blog/replace-your-api-layer-with-postgrest-rls" rel="noopener noreferrer"&gt;https://omenabyte.com/blog/replace-your-api-layer-with-postgrest-rls&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>api</category>
      <category>backend</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Two People Bought the Last Ticket. Both Got a Confirmation Email.</title>
      <dc:creator>Chizee</dc:creator>
      <pubDate>Sat, 10 Oct 2026 18:22:10 +0000</pubDate>
      <link>https://dev.to/chizee/two-people-bought-the-last-ticket-both-got-a-confirmation-email-ki9</link>
      <guid>https://dev.to/chizee/two-people-bought-the-last-ticket-both-got-a-confirmation-email-ki9</guid>
      <description>&lt;p&gt;One seat left. Row F, seat 12. Maya in Chicago and Leo in Denver hit Buy inside the same millisecond, and both of them get a confirmation email.&lt;/p&gt;

&lt;p&gt;Your code did nothing wrong. That's the part that should worry you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug is a gap, not an error
&lt;/h2&gt;

&lt;p&gt;Here's the flow nearly every application writes at some point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;seat = SELECT * FROM seats WHERE id = 'F12';
if (seat.status == 'available') {
    UPDATE seats SET status = 'sold' WHERE id = 'F12';
    send_confirmation_email();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read, check, write. Three steps, and nothing in that snippet is wrong on its own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqjhji87sbp0nuf13snyt.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqjhji87sbp0nuf13snyt.jpg" alt="Two lanes, Maya and Leo, each running read, check and write on the same row. An amber band between the two lanes shows Leo reading the row before Maya’s write lands, so both requests see the seat as available." width="800" height="983"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Both lanes do the same three correct steps. The amber band is the window where Leo’s read lands between Maya’s check and Maya’s write. Diagram: Omenabyte Intelligence.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now run two of them at once. Maya's request reads the row: &lt;code&gt;available&lt;/code&gt;. Before her write lands, Leo's request reads the same row. Maya hasn't committed yet, so from where Leo is standing the seat is still &lt;code&gt;available&lt;/code&gt;. Both checks pass. Both writes go through. Two emails, one chair.&lt;/p&gt;

&lt;p&gt;This is a race condition, and the useful thing to understand is that it isn't a crash. It's the ordinary result of two requests arriving at the same time. Nothing threw. Nothing logged a warning. The database accepted both writes because both writes were valid requests. The outcome depended entirely on which header arrived first, and nobody wrote that rule down.&lt;/p&gt;

&lt;p&gt;The name for the shape is &lt;strong&gt;check-then-act&lt;/strong&gt;: you made a decision based on a value, then acted on a world that had already moved. Every version of this bug, in every language, is that same gap between the reading and the writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the database didn't stop you
&lt;/h2&gt;

&lt;p&gt;It's tempting to assume the database should have caught this. It had all the information.&lt;/p&gt;

&lt;p&gt;Two things make the double sale possible, and both are deliberate.&lt;/p&gt;

&lt;p&gt;First, PostgreSQL's default isolation level is &lt;strong&gt;Read Committed&lt;/strong&gt;. A plain &lt;code&gt;SELECT&lt;/code&gt; sees a snapshot of the database as of the moment the query began. While Maya's transaction is open and uncommitted, her write is invisible to everyone else, which is exactly the point of running transactions. Isolation is supposed to hide in-progress work. So Leo legitimately sees &lt;code&gt;available&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Second, and this matters more, a plain &lt;code&gt;SELECT&lt;/code&gt; doesn't lock anything meaningful. Read Committed will happily let a hundred transactions read the same row simultaneously. Reads don't conflict with reads.&lt;/p&gt;

&lt;p&gt;So the database behaved correctly. It was asked to record a fact that was true when it was written, and it did.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi1713t0w6lo0w5lee3d4.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi1713t0w6lo0w5lee3d4.jpg" alt="A data centre aisle with two rows of white server cabinets facing each other" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The engine is doing what it was told. Nothing in the transaction was malformed; the problem is the shape of the operation, not the hardware running it. Photo: PiDatacenters / CC BY-SA 4.0&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix one: make the second request wait
&lt;/h2&gt;

&lt;p&gt;The direct approach is a row lock.&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;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'F12'&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- returns 'available', and the row is now locked for this transaction&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sold'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'F12'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FOR UPDATE&lt;/code&gt; locks the retrieved rows as though for update. While Maya holds that lock, Leo's &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; on the same row blocks. It doesn't fail, it doesn't return a stale value, it waits. When Maya commits, the lock releases and Leo's query proceeds and returns the updated row, which now says &lt;code&gt;sold&lt;/code&gt;. Leo gets an error instead of a ticket, and the order of the queue was never in doubt.&lt;/p&gt;

&lt;p&gt;This is pessimistic locking: assume contention, coordinate before anyone moves.&lt;/p&gt;

&lt;p&gt;Four things decide whether it actually works.&lt;/p&gt;

&lt;h3&gt;
  
  
  The lock only exists inside a transaction
&lt;/h3&gt;

&lt;p&gt;This is the mistake that makes people conclude locking doesn't work. In autocommit mode, a statement ends the moment it finishes, so the lock is released before your &lt;code&gt;UPDATE&lt;/code&gt; ever runs. The lock has to span the read, the decision, and the write. &lt;code&gt;BEGIN&lt;/code&gt; is not optional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lock the row you mean, not the table
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;WHERE status = 'pending' FOR UPDATE&lt;/code&gt; with no index on &lt;code&gt;status&lt;/code&gt; scans the table and locks every matching row on the way past. Lock on a primary key or another indexed column and you get a narrow lock instead of a queue for the whole table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftk6cg37qtk2z39rkmw90.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftk6cg37qtk2z39rkmw90.jpg" alt="An open frame network rack with cable management brackets and a raised floor with perforated vent tiles" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A lock is a reservation on one row. Aim it at a column without an index and you have reserved far more than you meant to. Photo: Carl Lender / CC BY 2.0&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Decide how long to wait
&lt;/h3&gt;

&lt;p&gt;An unbounded wait turns a lock into an outage. &lt;code&gt;FOR UPDATE NOWAIT&lt;/code&gt; fails immediately if the row is already locked (&lt;code&gt;55P03&lt;/code&gt;, &lt;code&gt;lock_not_available&lt;/code&gt;), and &lt;code&gt;lock_timeout&lt;/code&gt; caps the wait at a duration you chose. Either way you can turn the failure into a retry, a "just taken" message, or a queued job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Locks queue, and queues have a cost
&lt;/h3&gt;

&lt;p&gt;This is the source video's honest caveat, and it's worth repeating: if thousands of fans are fighting over the same section, requests pile up behind the lock. Each one is short. The total wait grows with the length of the line.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh3ic0sgnhnp7sg8y0c25.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh3ic0sgnhnp7sg8y0c25.jpg" alt="A large festival crowd standing in front of a lit stage at dusk" width="800" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contention on one row queues everyone. Contention spread across ten thousand different rows barely conflicts at all. Photo: Wvdp / CC0&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix two: delete the gap
&lt;/h2&gt;

&lt;p&gt;The better fix changes the shape of the operation so there's nothing to interleave.&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sold'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'F12'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'available'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One statement. The check and the write are the same event, evaluated against the same row, in the same instant. Maya's update matches the row and changes it. Leo's update arrives, finds no row that is still &lt;code&gt;available&lt;/code&gt;, and changes zero rows.&lt;/p&gt;

&lt;p&gt;Maya's update changes one row. Leo's changes zero. That difference is the entire result of the race, and the database hands it to you for free.&lt;/p&gt;

&lt;p&gt;Now the mechanism, because this is where most explanations stop and it's worth knowing why the &lt;code&gt;UPDATE&lt;/code&gt; is safe rather than lucky. This is straight from the PostgreSQL manual on Read Committed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The search condition of the command (the &lt;code&gt;WHERE&lt;/code&gt; clause) is re-evaluated to see if the updated version of the row still matches the search condition.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The part worth slowing down on: it isn't only that the two operations happen atomically, it's that if your &lt;code&gt;UPDATE&lt;/code&gt; finds a row another transaction has already modified, it waits for that transaction, then re-checks your &lt;code&gt;WHERE&lt;/code&gt; clause against the new version of the row. Leo's &lt;code&gt;AND status = 'available'&lt;/code&gt; is re-evaluated against the row Maya just committed, fails, and the row is skipped.&lt;/p&gt;

&lt;p&gt;So the guarantee isn't luck or a narrow timing window. It's a documented re-evaluation the engine performs for you.&lt;/p&gt;

&lt;p&gt;The consequence is that the operation can succeed silently with nothing to do, and that's a trap:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;seats&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sold'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'F12'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'available'&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the result set comes back empty, the seat went to someone else. If you don't check, your code sails on believing it booked a seat it never touched, and you've rebuilt the original bug with extra steps. Zero rows affected is your conflict signal, and it only helps if you read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix three: put the guarantee in the schema
&lt;/h2&gt;

&lt;p&gt;Application code has bugs. Your concurrency logic will be tested by people who don't know it's there, in code you haven't written yet.&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;tickets&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;seat_id&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;   &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;one_ticket_per_seat&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seat_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that constraint in place, two confirmed tickets for the same seat cannot exist in the table. Not "should not," not "unlikely to." The engine refuses the second insert and raises &lt;code&gt;23505&lt;/code&gt;, &lt;code&gt;unique_violation&lt;/code&gt;, regardless of what your application layer believes.&lt;/p&gt;

&lt;p&gt;Be clear about what this does and doesn't do. It does not make your checkout flow correct. It makes the final invariant unbreakable, which is a different and more valuable thing: it's the property that survives every future refactor, every new endpoint, every script somebody runs by hand at midnight.&lt;/p&gt;

&lt;p&gt;It also moves a decision into your error handling. The unique violation is a normal outcome under contention, not an exceptional one. Catch &lt;code&gt;23505&lt;/code&gt;, translate it into "that seat just went," and move on. If you let it bubble up, your loser gets a 500 for a situation the design anticipated.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each mechanism actually buys you
&lt;/h2&gt;

&lt;p&gt;These are not three ways to do the same thing. They handle the conflict at different moments and give the loser a different experience:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm404u008762u0ban2x3o.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm404u008762u0ban2x3o.jpg" alt="Three panels: SELECT FOR UPDATE, conditional UPDATE, and a UNIQUE constraint, each showing its SQL, what the losing request experiences, and what class of problem it guards." width="800" height="1071"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same seat, three mechanisms. The conditional UPDATE is the recommended default; the constraint is what stays true after every other layer has been refactored. Diagram: Omenabyte Intelligence.&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Conflict handled&lt;/th&gt;
&lt;th&gt;What the loser sees&lt;/th&gt;
&lt;th&gt;Fits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;before the decision&lt;/td&gt;
&lt;td&gt;waits, then sees the new value&lt;/td&gt;
&lt;td&gt;short multi-step decisions that need stable rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conditional &lt;code&gt;UPDATE ... WHERE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;at the attempted write&lt;/td&gt;
&lt;td&gt;zero rows affected&lt;/td&gt;
&lt;td&gt;state transitions with a clean precondition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unique constraint&lt;/td&gt;
&lt;td&gt;at insert&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;23505&lt;/code&gt; raised&lt;/td&gt;
&lt;td&gt;the final invariant, unconditionally&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A booking system uses all three. The conditional update takes the seat. The constraint backstops it. &lt;code&gt;FOR UPDATE&lt;/code&gt; earns its place when the decision needs several reads or writes against state that must not move underneath you, like validating a balance, writing a ledger entry, and updating an account.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts nobody puts in the cheat sheet
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Deadlocks are real and have a code
&lt;/h3&gt;

&lt;p&gt;If two transactions lock rows in opposite orders, one gets aborted with &lt;code&gt;40P01&lt;/code&gt;, &lt;code&gt;deadlock_detected&lt;/code&gt;. The fix is ordering: always acquire locks on multiple objects in the same sequence, and take the most restrictive lock you'll need first. PostgreSQL detects deadlocks and breaks them, but it's choosing a victim, not resolving your design.&lt;/p&gt;

&lt;h3&gt;
  
  
  Serialization failures need a retry loop
&lt;/h3&gt;

&lt;p&gt;Under Serializable, the engine can abort a transaction with &lt;code&gt;40001&lt;/code&gt;, &lt;code&gt;serialization_failure&lt;/code&gt;, even when there's no obvious conflict. That's the contract: you retry the whole transaction from a fresh read, with bounded attempts and backoff. Retrying from where you left off defeats the purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isolation level changes what &lt;code&gt;FOR UPDATE&lt;/code&gt; does on conflict
&lt;/h3&gt;

&lt;p&gt;Under Read Committed, a blocked &lt;code&gt;FOR UPDATE&lt;/code&gt; waits and then returns the updated row, as above. Under Repeatable Read or Serializable, it throws instead, because the row changed since your transaction's snapshot. Both are defensible. You just have to know which one you're getting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retries duplicate side effects
&lt;/h3&gt;

&lt;p&gt;If a transaction can charge a card, send an email, or publish a message, a retry can do it twice. Keep external I/O outside the locked transaction wherever you can, and make the operations that must cross the boundary idempotent with a key the receiver understands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sometimes you want the queue to be skippable
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;SELECT ... FOR UPDATE SKIP LOCKED&lt;/code&gt; doesn't wait for a locked row, it moves on to the next one. For a work queue where workers are draining a table of jobs, that's the right behaviour, and it's the standard pattern for job tables. It's the wrong behaviour for a seat, where you want that specific row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two rules that outlive the example
&lt;/h2&gt;

&lt;p&gt;Keep your checks inside the write, and keep the final guarantee in the schema.&lt;/p&gt;

&lt;p&gt;The first rule is about where the decision happens: if a condition decides whether a write is allowed, that condition belongs in the &lt;code&gt;WHERE&lt;/code&gt; clause, not in an &lt;code&gt;if&lt;/code&gt; above it. The second is about who can be trusted: your application is code you're still writing, but a constraint is a property the engine will enforce against every caller forever, including the ones you haven't imagined.&lt;/p&gt;

&lt;p&gt;Together they close the gap from both ends. The write stops being interruptible, and the outcome stops depending on anything the write got right.&lt;/p&gt;

&lt;p&gt;Which has cost you more time, the bug, or the incident review after it?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The seat-booking framing, the Maya and Leo example, and the three-mechanism structure are from a &lt;a href="https://www.facebook.com/reel/1895499224957143" rel="noopener noreferrer"&gt;Knowmore&lt;/a&gt; video on database concurrency, which states its lock and constraint guarantees accurately. The mechanism-level detail added here is from the &lt;a href="https://www.postgresql.org/docs/current/transaction-iso.html" rel="noopener noreferrer"&gt;PostgreSQL 18 manual&lt;/a&gt;: the WHERE re-evaluation under Read Committed, lock ordering and deadlock codes, NOWAIT and SKIP LOCKED, and the isolation-level differences. Photography from Wikimedia Commons under the licences noted in each caption; both diagrams were drawn for this article.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on omenabyte.com -&amp;gt; &lt;a href="https://omenabyte.com/blog/the-gap-between-checking-and-writing" rel="noopener noreferrer"&gt;https://omenabyte.com/blog/the-gap-between-checking-and-writing&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>"Find What's Nearby" Doesn't Need a Separate GIS Stack</title>
      <dc:creator>Chizee</dc:creator>
      <pubDate>Sun, 04 Oct 2026 10:08:59 +0000</pubDate>
      <link>https://dev.to/chizee/find-whats-nearby-doesnt-need-a-separate-gis-stack-2hbn</link>
      <guid>https://dev.to/chizee/find-whats-nearby-doesnt-need-a-separate-gis-stack-2hbn</guid>
      <description>&lt;p&gt;Store finders, delivery radius checks, geofencing — any "what's near this point" feature looks like it needs specialized spatial infrastructure. Done naively (raw coordinate math checked against every row), it genuinely does fall over past a few thousand points. Done with &lt;code&gt;PostGIS&lt;/code&gt;, it's an index like any other.&lt;/p&gt;

&lt;p&gt;PostGIS isn't a lightweight workaround, either — it's the de facto industry standard most dedicated GIS tooling gets benchmarked against.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&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="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;postgis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;coffee_shops&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;        &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;location&lt;/span&gt;  &lt;span class="n"&gt;geography&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4326&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_coffee_shops_location&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;coffee_shops&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIST&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;GiST&lt;/code&gt; index draws simple bounding boxes around your geometries first, so Postgres can instantly discard millions of points that obviously aren't close, before running the precise (and expensive) distance math on the small set that remain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The query
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;coffee_shops&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ST_DWithin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ST_SetSRID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ST_MakePoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;122&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;77&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;4326&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;geography&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;1000&lt;/span&gt;  &lt;span class="c1"&gt;-- meters&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The trap that's actually worth knowing about
&lt;/h2&gt;

&lt;p&gt;I went looking for the "obvious" PostGIS gotcha (mismatched SRIDs throwing hard errors) and, tested directly against a live PostGIS instance, that specific fear turned out to be overstated — casting straight to &lt;code&gt;geography&lt;/code&gt; without an explicit SRID mostly just works, since geography only ever supports SRID 4326 anyway.&lt;/p&gt;

&lt;p&gt;The trap that's real and far more dangerous: PostGIS does &lt;strong&gt;not&lt;/strong&gt; validate that your coordinates are actually in range. Feed it projected/UTM meters where it expected longitude/latitude degrees, and it silently &lt;em&gt;coerces&lt;/em&gt; the values into valid range with nothing but a quiet &lt;code&gt;NOTICE&lt;/code&gt; — no error, no loud failure, just garbage coordinates that look plausible until someone notices the map is wrong. If you're piping coordinates in from another system, double-check the units before they ever hit this table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this beats a separate GIS engine
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It's the standard, not a substitute — documentation and tooling assume PostGIS by default.&lt;/li&gt;
&lt;li&gt;Spatial joins work against ordinary relational tables in the same query.&lt;/li&gt;
&lt;li&gt;Decades of production hardening, not a new or experimental extension.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where dedicated GIS tooling still wins
&lt;/h2&gt;

&lt;p&gt;Heavy cartographic rendering or raster analysis — satellite imagery, terrain modeling — which is a genuinely different workload from querying point and polygon data. For "what's near this address," you likely already have everything you need.&lt;/p&gt;




&lt;p&gt;This is one of eight infrastructure swaps in &lt;strong&gt;Just Use Postgres&lt;/strong&gt;, a 24-page field manual on replacing MongoDB, Redis, Elasticsearch, Pinecone, and more with the database you're probably already running. Every recipe in it — including this one — was run against a live Postgres instance before it went in the book.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Get the field manual&lt;/strong&gt; — launch price &lt;strong&gt;$14&lt;/strong&gt; instead of standing up a tile server for a distance query: &lt;a href="https://payhip.com/omenabyte" rel="noopener noreferrer"&gt;https://payhip.com/omenabyte&lt;/a&gt;&lt;br&gt;
🐳 Or take the &lt;strong&gt;$24 bundle&lt;/strong&gt; with the full &lt;code&gt;docker-compose up&lt;/code&gt; starter repo — all 8 modules as tested, runnable migrations + seed data: &lt;a href="https://4693433176360.gumroad.com/" rel="noopener noreferrer"&gt;https://4693433176360.gumroad.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Read this on omenabyte.com → &lt;a href="https://omenabyte.com/blog/replace-gis-tools-with-postgis" rel="noopener noreferrer"&gt;https://omenabyte.com/blog/replace-gis-tools-with-postgis&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>gis</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Eight Layers Between an Attacker and Your Data. You've Only Configured One.</title>
      <dc:creator>Chizee</dc:creator>
      <pubDate>Fri, 02 Oct 2026 23:23:09 +0000</pubDate>
      <link>https://dev.to/chizee/eight-layers-between-an-attacker-and-your-data-youve-only-configured-one-2n51</link>
      <guid>https://dev.to/chizee/eight-layers-between-an-attacker-and-your-data-youve-only-configured-one-2n51</guid>
      <description>&lt;p&gt;When you think about network security, you probably picture one thing. A firewall. Something blocking hackers on the other side of a wall.&lt;/p&gt;

&lt;p&gt;That's one piece. There are eight layers in the model, and the firewall is the one least likely to be the reason you get breached, precisely because it's the one everybody configures.&lt;/p&gt;

&lt;p&gt;Think about a bank. It doesn't lock the front door and call it done. There's a guard, a vault, cameras, an alarm, probably a second vault inside the first. Getting past one obstacle doesn't get you the money. You have to get past all of them.&lt;/p&gt;

&lt;p&gt;So follow an attacker from the outside in and see what actually stops them at each stage.&lt;/p&gt;

&lt;p&gt;One thing worth saying first, because it trips people up constantly: these eight are a control stack, not the OSI model's seven network layers. Some names overlap and the numbering does not match at all. When I say "the application layer" here, I mean the sixth barrier in this list, not OSI layer 7.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffbetod0z36x361ehmo0t.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffbetod0z36x361ehmo0t.jpg" alt="Diagram of eight stacked security layers, from physical security at the bottom to monitoring and incident response at the top, each with a one-line job and what it stops" width="800" height="1232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The eight barriers, in the order an attacker meets them, with what each one exists to stop. Diagram: Omenabyte Intelligence.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Physical security
&lt;/h2&gt;

&lt;p&gt;Before firewalls or passwords, a simpler question. Can someone walk up and touch the hardware?&lt;/p&gt;

&lt;p&gt;Locked doors, cameras, access badges, guards. All of it exists to stop someone plugging directly into a server or walking out with a drive under their arm. It sounds almost too obvious to mention, which is why it's the foundation everything else sits on. You can have the best firewall in the world, and if anyone can stroll into the server room, none of it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: Perimeter security
&lt;/h2&gt;

&lt;p&gt;The network is physically protected, but attackers don't need to enter the building. They come through the connection.&lt;/p&gt;

&lt;p&gt;This is the firewall, and the right mental model is a checkpoint rather than a wall. Every connection trying to enter or leave gets checked against rules. Some traffic is allowed. Some is blocked because it came from an untrusted source, used a restricted port, or failed the network's policy. A company might let employees browse the web while blocking unexpected inbound connections to internal servers.&lt;/p&gt;

&lt;p&gt;Firewalls also police traffic between internal zones, not just between you and the internet. That matters for what comes next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Network segmentation
&lt;/h2&gt;

&lt;p&gt;An attacker got past the perimeter. Do they now have everything?&lt;/p&gt;

&lt;p&gt;Only if your network is one flat space. Well-designed networks are broken into zones with VLANs, subnets and access control lists so different parts can't freely talk to each other. Employee laptops in one zone. Finance systems in another. Servers holding sensitive data in a third.&lt;/p&gt;

&lt;p&gt;The payoff is specific. Compromising a single employee laptop doesn't hand an attacker a path to the database. Same logic as keycard doors inside a building: getting through the front entrance doesn't open every door.&lt;/p&gt;

&lt;p&gt;This is where the model starts earning its keep, because it reframes what you're buying. Network security isn't only about keeping attackers out. It's about limiting what they can reach once they're in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm60jew4i1f9c0skx1vo8.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm60jew4i1f9c0skx1vo8.jpg" alt="A structured cabling closet with dense bundles of patch cables terminating at panels" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Segmentation is physical before it is logical: every zone boundary lands somewhere, on a switch port and a cable run. Photo: Wikimedia Commons / Public domain.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: Identity and access
&lt;/h2&gt;

&lt;p&gt;Controlling where someone can go isn't enough. You also need to know who they are and what they're allowed to touch.&lt;/p&gt;

&lt;p&gt;Two ideas live here, and this is the pair people mix up most.&lt;/p&gt;

&lt;p&gt;Authentication asks who you are. Proving identity, usually with a password, ideally backed by a second factor like a code on your phone.&lt;/p&gt;

&lt;p&gt;Authorization asks what you may do. A regular employee account shouldn't open the same systems an IT administrator can, even though both logged in successfully.&lt;/p&gt;

&lt;p&gt;That gap is where least privilege comes from. Give people access to only what they actually need, nothing more. If someone does steal a login, least privilege limits the damage to whatever that one account could touch. Even inside a segmented zone, with valid-looking credentials, something is still deciding what's reachable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr1uwaupyln6zq867dve1.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr1uwaupyln6zq867dve1.jpg" alt="Diagram contrasting authentication, which establishes who you are, with authorization, which decides what you may do and enforces least privilege" width="800" height="1295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The distinction the source video flags as the easiest one to lose track of. Diagram: Omenabyte Intelligence.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 5: Endpoint security
&lt;/h2&gt;

&lt;p&gt;Everything so far has been the network. But there's still a device sitting at the end of it, and that device needs its own armour.&lt;/p&gt;

&lt;p&gt;Antivirus, host-based firewalls, system updates, plus detection tools that watch for suspicious behaviour. Here's the uncomfortable part. A perfectly secured network can be completely compromised through one infected laptop. Someone downloads a malicious attachment or plugs in a stray USB drive. The threat isn't trying to break down the front door anymore. It's already inside, running code on a device that was allowed to be there all along.&lt;/p&gt;

&lt;p&gt;That's why the endpoint is a primary target rather than a passive tool sitting safely behind the network's defences.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2uqghx70adnrf4fexcrd.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2uqghx70adnrf4fexcrd.jpg" alt="An open network rack filled with bundled patch cables and an orange fibre run" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Physical and perimeter controls are the two layers most organisations do get right, which is exactly why attackers work elsewhere. Photo: Yann / CC BY-SA 3.0.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 6: Application security
&lt;/h2&gt;

&lt;p&gt;Protecting the device isn't the end of it either, because the software on that device can have flaws of its own.&lt;/p&gt;

&lt;p&gt;A login form that doesn't check its inputs. Software that hasn't been patched. A bug that lets someone bypass a permission check entirely. The network is fine, the device is fine, and the application is the way in.&lt;/p&gt;

&lt;p&gt;The controls are secure coding, vulnerability testing before attackers get there, authentication built into the application, input validation that doesn't trust what users type, and in some cases a web application firewall filtering traffic aimed at the app rather than the network.&lt;/p&gt;

&lt;p&gt;The distinction worth holding on to is this. Network security protects the path data travels on. Application security protects the software using that path. You can lock down every road into a city and still have a building with a broken door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 7: Data security
&lt;/h2&gt;

&lt;p&gt;Every layer so far was about stopping an attacker getting closer. This one matters if all of them fail, because this is the thing they were actually after.&lt;/p&gt;

&lt;p&gt;Encryption in transit protects data while it moves, so an interception yields something unreadable without the key. Encryption at rest protects it while it sits on a server doing nothing. Access controls decide who can open it. Backups mean that if data is deleted, corrupted, or held for ransom, a copy exists elsewhere. Data classification sorts information by sensitivity so the most critical material gets the strictest handling.&lt;/p&gt;

&lt;p&gt;Which leads to the reframe this layer buys you. Security isn't only about stopping someone getting in. It's also about making sure that if they do get in and walk away with something, it's useless to them. Encrypted data without the key is noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 8: Monitoring and incident response
&lt;/h2&gt;

&lt;p&gt;An attacker made it past everything. What now?&lt;/p&gt;

&lt;p&gt;Logs record activity. Intrusion detection tools flag suspicious behaviour. SIEM platforms pull all of it into one place and look for patterns a human would never catch on their own. A login from a country the employee has never visited. A server suddenly shipping gigabytes outbound at 3am.&lt;/p&gt;

&lt;p&gt;When something trips, incident response takes over. Detect the threat. Investigate what actually happened. Contain it before it spreads. Then recover, restoring systems, patching whatever let the attacker in, and learning from it.&lt;/p&gt;

&lt;p&gt;This layer is what makes the whole system honest. Layers 1 through 7 are prevention, all of them betting an attack can be stopped. Layer 8 accepts that prevention fails, and makes sure failure isn't the end of the story.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flsvysslkdpumc3u3b6wy.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flsvysslkdpumc3u3b6wy.jpg" alt="An operations centre with rows of operators at workstations facing a wall of monitoring screens" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Monitoring only pays off if someone is watching and empowered to act on what the screens say. Photo: Zhonghua Minguo Zongtongfu / CC BY 2.0.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this model quietly breaks
&lt;/h2&gt;

&lt;p&gt;Eight layers is a good map. Here's what it leaves out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attacks chain layers, they don't pick one
&lt;/h3&gt;

&lt;p&gt;Phishing gets in through the application layer as a malicious link, executes as malware on the endpoint, performs ARP spoofing at the data link layer to intercept traffic, then moves sideways because the network was never segmented. Defence in depth works because the chain gets interrupted somewhere, not because each layer is individually perfect. Treating the layers as independent hides the part that actually matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Controls get pointed at the wrong layer
&lt;/h3&gt;

&lt;p&gt;This is the most common engineering error. A firewall rule operates on IP addresses and ports, so it cannot stop SQL injection. Injection is an application-layer problem wearing a valid-looking HTTP request, well-formed traffic on an allowed port. Teams then conclude the firewall failed, when the firewall was never the right tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  There's more than one thing called a firewall
&lt;/h3&gt;

&lt;p&gt;Traditional firewalls work at OSI's network and transport layers, filtering by address and port. A web application firewall inspects HTTP content and is also called a firewall. Same word, different layer, different job. Assuming you have "the firewall" covered when you own one of the two is how Layer 6 stays open.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encryption isn't one thing either
&lt;/h3&gt;

&lt;p&gt;IPsec at OSI's network layer protects all traffic between two endpoints, transparently to applications. TLS protects one application's session. Same goal, different scope, and reaching for the wrong one is a classic layer mismatch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nobody implements all eight equally well
&lt;/h3&gt;

&lt;p&gt;That's fine, and pretending otherwise is worse than admitting it. The useful exercise is knowing which layer you're weakest at. For most small teams, it isn't the perimeter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical version
&lt;/h2&gt;

&lt;p&gt;The skill worth taking away isn't memorising eight names. It's matching the control to the layer of the problem.&lt;/p&gt;

&lt;p&gt;Network path problem, think firewall or segmentation. Admission problem, think network access control. Malicious traffic that needs blocking inline, think intrusion prevention. Endpoint behaviour after execution, think EDR. Sensitive data leaving through email or cloud sharing, think DLP. Injection or cross-site scripting in a web app, think input validation and a WAF.&lt;/p&gt;

&lt;p&gt;Get the layer right and the tool choice mostly makes itself. Get it wrong and you'll spend money on the wrong control, then wonder why the problem is still there.&lt;/p&gt;

&lt;p&gt;Which of those eight did you last actually look at? For most teams it's the firewall. For most teams the firewall was never the layer that was going to fail.&lt;/p&gt;

&lt;p&gt;If you made it this far, you now know more about network security than most people who click "remind me later" on a software update.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The eight-layer framing is from &lt;a href="https://www.facebook.com/reel/1138922678706848" rel="noopener noreferrer"&gt;"Every Layer of Network Security Explained"&lt;/a&gt; by Tech Learning Zone. The material on chained attacks, misplaced controls, IPsec versus TLS scope, and the NIST SP 800-61 incident-response framing is added editorial context, not from that video. Photography from Wikimedia Commons under the licences noted in each caption; both diagrams were drawn for this article.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on omenabyte.com -&amp;gt; &lt;a href="https://omenabyte.com/blog/every-layer-of-network-security" rel="noopener noreferrer"&gt;https://omenabyte.com/blog/every-layer-of-network-security&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>networking</category>
      <category>cybersecurity</category>
      <category>devops</category>
    </item>
    <item>
      <title>He Put 5,000 Users on a 12-Dollar Server. The Database Was Fine.</title>
      <dc:creator>Chizee</dc:creator>
      <pubDate>Wed, 30 Sep 2026 23:25:20 +0000</pubDate>
      <link>https://dev.to/chizee/he-put-5000-users-on-a-12-dollar-server-the-database-was-fine-gei</link>
      <guid>https://dev.to/chizee/he-put-5000-users-on-a-12-dollar-server-the-database-was-fine-gei</guid>
      <description>&lt;p&gt;The server cost twelve dollars a month. One CPU core, two gigabytes of RAM.&lt;/p&gt;

&lt;p&gt;On it, a developer built a Twitter-style social API and loaded it with 50,000 users, 500,000 posts, and two million likes. The whole thing ran on that one machine: nginx, Node, and Postgres, no separate database box.&lt;/p&gt;

&lt;p&gt;Then he rented a second server for four hours and used it to attack the first one with k6, spinning up virtual users by the thousand. Each one ran a loop that looks roughly like a person. Load the feed, wait a second or two, like something, maybe post something, load the feed again.&lt;/p&gt;

&lt;p&gt;Ten users did nothing. So did fifty, a hundred, two hundred, a thousand.&lt;/p&gt;

&lt;p&gt;At two thousand, the tail latency started moving. The average was still clean. P95 and P99 were climbing.&lt;/p&gt;

&lt;p&gt;At three thousand he'd crossed his own failure threshold and backed down to 2,500. That was the ceiling: roughly 232 requests per second, P95 around 288 milliseconds.&lt;/p&gt;

&lt;p&gt;Here's the part worth sitting with. RAM was still under a gigabyte. Postgres was completely fine. The CPU was at 90%.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottleneck wasn't where he expected it
&lt;/h2&gt;

&lt;p&gt;Most people building a social app spend their early architecture budget on the database. Replicas, connection poolers, a caching layer, maybe a queue. The assumption is that the data tier buckles first under real concurrency.&lt;/p&gt;

&lt;p&gt;Not here. At the limit, the database was sitting idle-ish and the single CPU core was the wall.&lt;/p&gt;

&lt;p&gt;That makes sense once you look at what each request costs. Assembling a feed means parsing a request, authenticating it, querying, building objects, and serializing JSON. All of that is CPU work. Every request takes a fixed slice of that one core. Divide the load test's throughput into its CPU budget and you land at roughly four milliseconds of CPU per request.&lt;/p&gt;

&lt;p&gt;Four milliseconds is the number that sets the ceiling. Nothing about it involves Postgres.&lt;/p&gt;

&lt;p&gt;It also explains why the averages looked fine while the tail didn't. A saturated CPU queues work. Most requests arrive when the queue is short and get served quickly. The ones that land behind a backlog wait proportionally longer. Averages hide that, because the queue is only deep some of the time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6cyi661bu7sxhfvd8d8z.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6cyi661bu7sxhfvd8d8z.jpg" alt="Every request costs a fixed slice of CPU: parsing, auth, query building, JSON serialization. At the ceiling th" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every request costs a fixed slice of CPU: parsing, auth, query building, JSON serialization. At the ceiling that worked out to roughly four milliseconds per request.&lt;/em&gt; Photo: &lt;a href="https://commons.wikimedia.org/wiki/File:DEC_Alpha_21066-AA_CPU_die_shot.jpg" rel="noopener noreferrer"&gt;Martijn Boer&lt;/a&gt; / Public domain&lt;/p&gt;

&lt;p&gt;And nobody experiences your average response time. They experience the worst request in their session. If a page fires twenty requests and your P99 is three seconds, that user has a real chance of feeling the app stall on every load. P95 and P99 aren't vanity metrics for dashboards nobody reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two changes, roughly double the capacity
&lt;/h2&gt;

&lt;p&gt;He resisted the obvious move. No Redis, no migration to a bigger box.&lt;/p&gt;

&lt;p&gt;First he cached the feed response inside Node, with a one-second TTL. That's the whole change. His reasoning was practical: if you're running one server anyway, you already have the memory, and using it is free. You don't need to introduce a separate service to hold a value for one second.&lt;/p&gt;

&lt;p&gt;It took him from 2,500 to about 4,000 concurrent users.&lt;/p&gt;

&lt;p&gt;Why one second works so well comes down to traffic concentration. The feed is the same response for everyone during that window. If most of your 232 requests per second are asking for the same thing, collapsing thousands of identical requests into a handful of database queries and render passes removes almost all of that CPU cost at once. The gain is proportional to how much your traffic asks for the same thing. A global feed caches beautifully. Per-user data caches far worse.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmo450g62vgexu2090iwd.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmo450g62vgexu2090iwd.jpg" alt="Turns out you do not need much machine. The whole API stack ran on one core and two gigs of RAM while serving " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Turns out you do not need much machine. The whole API stack ran on one core and two gigs of RAM while serving thousands of concurrent users.&lt;/em&gt; Photo: &lt;a href="https://commons.wikimedia.org/wiki/File:RaspberryPi.jpg" rel="noopener noreferrer"&gt;Jwrodgers&lt;/a&gt; / CC BY-SA 3.0&lt;/p&gt;

&lt;p&gt;Then he moved that cache out of Node and into nginx. This added less than the first change did, which is worth noting: the easy win was the one inside the application, and the second change bought the remaining thousand. When nginx answers from its own cache the request never enters the Node process at all, so there's no request parsing, no middleware chain, no JSON serialization, and no garbage collection pressure from thousands of response objects. The CPU that was pegged at 90% is only busy on cache misses now.&lt;/p&gt;

&lt;p&gt;That pushed him past 5,000 concurrent users on the same twelve-dollar machine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywq3xejfj43k2zo2veqr.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywq3xejfj43k2zo2veqr.jpg" alt="Moving the cache out of Node and into the reverse proxy meant cached requests never entered the application pr" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Moving the cache out of Node and into the reverse proxy meant cached requests never entered the application process at all.&lt;/em&gt; Photo: &lt;a href="https://commons.wikimedia.org/wiki/File:Raspberry_Pi_4_on_a_network_switch.jpg" rel="noopener noreferrer"&gt;RAIL P (RAIL.PHOTOGRAPHY)&lt;/a&gt; / CC0&lt;/p&gt;

&lt;p&gt;His rule for this is the useful part: move the cache as close to your users as you can reasonably get it. It sounds like a slogan until you've watched it happen. Every hop you remove from the request path is CPU you don't spend, and CPU was the entire problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb07xhlktbxbw0ztqq3zf.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb07xhlktbxbw0ztqq3zf.jpg" alt="Amber is where the server started. The first cache bought 1,500 users; moving it into nginx bought the next 1," width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amber is where the server started. The first cache bought 1,500 users; moving it into nginx bought the next 1,000. The fading tail on the last bar is deliberate: the test stopped at 5,000, so that end is a floor and not a ceiling.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Chart by Omenabyte Intelligence. Figures as measured by Arjaythedev.&lt;/p&gt;

&lt;h2&gt;
  
  
  The extrapolation, and where it gets shaky
&lt;/h2&gt;

&lt;p&gt;Five thousand concurrent users doesn't mean five thousand customers. Real people don't all use an app at the same instant. Based on typical usage patterns he estimates that concurrency level would correspond to somewhere around 25,000 to 30,000 daily active users.&lt;/p&gt;

&lt;p&gt;That's the honest version of the headline, and it's worth being careful with. "A twelve-dollar server handles 5,000 users" is true for this application, this load profile, and this definition of handling. Swap the workload for video transcoding, per-request image resizing, or LLM inference and the number moves by orders of magnitude, because those burn vastly more CPU per request. The transferable part of this experiment isn't the number. It's the method, and the two fixes.&lt;/p&gt;

&lt;p&gt;The method deserves more attention than it usually gets. He measured before he optimized. He found the actual bottleneck instead of the assumed one, and he fixed that specific thing. Most teams never do the first step, and then spend real money on the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the video skips
&lt;/h2&gt;

&lt;p&gt;A single-machine setup that passes a load test still has problems the test doesn't cover.&lt;/p&gt;

&lt;p&gt;When a one-second cache entry expires, every concurrent request misses at the same moment and they all stampede the database together. nginx has a real fix for this: &lt;code&gt;proxy_cache_lock on&lt;/code&gt;. When an entry expires, exactly one request goes upstream while the rest wait for it and then get served from the newly filled cache. Hand-rolled in-process caches rarely bother, which is why a one-second TTL on a hot key can produce a nasty sawtooth in your database load. The cache helps on average and hurts in bursts.&lt;/p&gt;

&lt;p&gt;In-process caches also don't survive a restart. A deploy empties them, and if a cold cache means every request hits the database at once, your deploy just created a self-inflicted traffic spike. Run Node in cluster mode or more than one container and each process holds its own copy with its own idea of the truth. Fine for one global feed. Actively wrong for sessions, rate-limit counters, or anything per-user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvhf4amcrh59ymlpyuf4w.jpg" class="article-body-image-wrapper"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvhf4amcrh59ymlpyuf4w.jpg" alt="The migration this experiment argues against. Big iron solves the problem money can solve, not the one measure" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The migration this experiment argues against. Big iron solves the problem money can solve, not the one measurement would have found.&lt;/em&gt; Photo: &lt;a href="https://commons.wikimedia.org/wiki/File:Virginia_Tech_-_data_center.jpg" rel="noopener noreferrer"&gt;Christopher Bowns&lt;/a&gt; / CC BY-SA 2.0&lt;/p&gt;

&lt;p&gt;And one machine is one point of failure. The load test measured capacity. It measured nothing about availability. A deploy is downtime, a hardware fault is an outage, and there's no redundancy anywhere in that stack. Sometimes that's an acceptable trade for a product at this stage. It should be a decision you made on purpose rather than something you discover at 2am.&lt;/p&gt;

&lt;p&gt;There's also the load model itself. k6 virtual users follow a fixed loop, which makes them far more polite than real people. Real traffic arrives in bursts. A single scheduled notification or one post that gets picked up can spike you to a multiple of your baseline within seconds. Capacity planning has to plan for peak rather than mean, which is the standard guidance for sizing load tests: test at your projected peak, then push past it to find your margin.&lt;/p&gt;

&lt;h2&gt;
  
  
  The instinct this should push back on
&lt;/h2&gt;

&lt;p&gt;Reaching for distributed infrastructure is usually an anxiety response, not an engineering one. The reasoning goes: this is a real product now, real products have scale problems, and scale problems need Kubernetes plus a queue plus a managed cache tier plus a service mesh. Money gets spent on a topology diagram before anyone has measured a single bottleneck.&lt;/p&gt;

&lt;p&gt;That twelve-dollar server had one CPU core and it served over 5,000 concurrent users. It got there with two changes that cost nothing, because the person running the experiment had the discipline to find out what was actually limited before deciding how to fix it.&lt;/p&gt;

&lt;p&gt;You might not need a distributed system. You might need to know what your CPU is doing.&lt;/p&gt;

&lt;p&gt;The uncomfortable part is that the only way to find out which one you need is to run the test. Until you have, the migration you're planning is a guess about a number you don't know.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Source: load-test figures are from &lt;a href="https://m.facebook.com/watch/?v=27472515385758505" rel="noopener noreferrer"&gt;How many users can a $12 server support?&lt;/a&gt; by Arjaythedev, a hands-on k6 experiment on a $12/month VPS (1 vCPU, 2 GB RAM).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>performance</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Stop Recomputing Your Dashboard on Every Page Load</title>
      <dc:creator>Chizee</dc:creator>
      <pubDate>Sun, 27 Sep 2026 10:11:50 +0000</pubDate>
      <link>https://dev.to/chizee/stop-recomputing-your-dashboard-on-every-page-load-5b60</link>
      <guid>https://dev.to/chizee/stop-recomputing-your-dashboard-on-every-page-load-5b60</guid>
      <description>&lt;p&gt;A dashboard that runs a heavy aggregation against live tables works fine in a demo and then crawls in production, because every single page load recalculates the same numbers from scratch. The usual fix is piping everything into a dedicated warehouse like Snowflake purely so the dashboard has something fast to read.&lt;/p&gt;

&lt;p&gt;For a huge share of "internal dashboard" use cases, a materialized view does the same job with zero new infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-bake the aggregation
&lt;/h2&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="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;daily_revenue&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;daily_revenue&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;day&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That unique index is not optional decoration — I confirmed this directly by dropping it and trying to refresh: Postgres refuses outright with &lt;code&gt;cannot refresh materialized view "daily_revenue" concurrently&lt;/code&gt;, and tells you exactly why. It's the single most common way this recipe trips people up, so build the index in from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refresh without locking out readers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;REFRESH&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;daily_revenue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CONCURRENTLY&lt;/code&gt; recalculates in the background and hot-swaps only the changed rows into place — anyone reading the view mid-refresh sees the old data until the swap completes, never a lock, never a blank dashboard.&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;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;daily_revenue&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;day&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your dashboard just reads this like any other table. No ETL pipeline, no separate job runner — the "transform" step &lt;em&gt;is&lt;/em&gt; the view definition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put it on a schedule with zero extra infrastructure
&lt;/h2&gt;

&lt;p&gt;If you have &lt;code&gt;pg_cron&lt;/code&gt; available:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'refresh-daily-revenue'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'*/15 * * * *'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'REFRESH MATERIALIZED VIEW CONCURRENTLY daily_revenue'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scheduled refresh, running inside the database you already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this beats a dedicated warehouse for most dashboards
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No ETL pipeline to babysit — the transform step is a SQL view.&lt;/li&gt;
&lt;li&gt;Dashboards stay fast as data grows, since they're reading pre-computed numbers, not raw transaction history.&lt;/li&gt;
&lt;li&gt;You control the refresh cadence directly — hourly, nightly, or on demand, it's one command either way.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Snowflake still wins
&lt;/h2&gt;

&lt;p&gt;Ad-hoc analytical queries across terabytes of historical data, joining many large fact tables in ways your operational schema was never modeled for. If your "warehouse" need is really "make this one dashboard fast," you may not need one.&lt;/p&gt;




&lt;p&gt;This is one of eight infrastructure swaps in &lt;strong&gt;Just Use Postgres&lt;/strong&gt;, a 24-page field manual on replacing MongoDB, Redis, Elasticsearch, Pinecone, and more with the database you're probably already running. Every recipe in it — including this one — was run against a live Postgres instance before it went in the book.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Get the field manual&lt;/strong&gt; — launch price &lt;strong&gt;$14&lt;/strong&gt; instead of paying a warehouse to pre-compute two numbers: &lt;a href="https://payhip.com/omenabyte" rel="noopener noreferrer"&gt;https://payhip.com/omenabyte&lt;/a&gt;&lt;br&gt;
🐳 Or take the &lt;strong&gt;$24 bundle&lt;/strong&gt; with the full &lt;code&gt;docker-compose up&lt;/code&gt; starter repo — all 8 modules as tested, runnable migrations + seed data: &lt;a href="https://4693433176360.gumroad.com/" rel="noopener noreferrer"&gt;https://4693433176360.gumroad.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Read this on omenabyte.com → &lt;a href="https://omenabyte.com/blog/replace-snowflake-dashboards-with-materialized-views" rel="noopener noreferrer"&gt;https://omenabyte.com/blog/replace-snowflake-dashboards-with-materialized-views&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>analytics</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>You're Doing Vibe-Coding Wrong. Here's the 17-Point Fix.</title>
      <dc:creator>Chizee</dc:creator>
      <pubDate>Sun, 20 Sep 2026 10:59:39 +0000</pubDate>
      <link>https://dev.to/chizee/youre-doing-vibe-coding-wrong-heres-the-17-point-fix-e9h</link>
      <guid>https://dev.to/chizee/youre-doing-vibe-coding-wrong-heres-the-17-point-fix-e9h</guid>
      <description>&lt;p&gt;Nobody tells you this about the app you shipped last weekend: it's probably leaking.&lt;/p&gt;

&lt;p&gt;Not "might be" leaking. Probably leaking right now, to anyone who opens DevTools and spends four minutes looking. That isn't a scare tactic. Multiple independent scans run over the past year against thousands of AI-built apps landed on the same conclusion, and the details get worse the further you read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the scanners actually found
&lt;/h2&gt;

&lt;p&gt;Three research groups scanned thousands of AI-generated web apps built with Lovable, Bolt, v0, Replit, and Cursor. The numbers line up uncomfortably well.&lt;/p&gt;

&lt;p&gt;Vibe App Scanner ran 1,215 scans across 1,119 distinct apps between December 2025 and August 2026. 10.4% had at least one critical issue. 31.1% had something critical or high severity. Of the 359 apps using Supabase, which was 29.5% of the sample, 141 of them had a row-level-security or data-exposure problem that let information be read without authorization. That's 39.3%.&lt;/p&gt;

&lt;p&gt;Symbiotic Security crawled 65,643 URLs, confirmed 1,085 Supabase-backed sites, and scanned 1,072 of them. They logged 6,185 vulnerabilities. 98% of sites had at least one. 16% had a critical one. Only 26 of the 1,072 came back completely clean.&lt;/p&gt;

&lt;p&gt;Worth stating plainly: that 98% applies to Supabase-backed apps in their sample, not every AI-built app everywhere. And Vibe App Scanner flags its own bias in the other direction — their apps were voluntarily submitted by owners who already suspected a problem, so those rates over-represent risk, and 83% of their scans were the reduced quick-scan set. Their words: every prevalence number is a floor, not a ceiling. Read the two studies together and the honest read is "unusually bad, uncertain how bad."&lt;/p&gt;

&lt;p&gt;Two percent clean. That's the rate in the larger sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  The specific failures
&lt;/h2&gt;

&lt;p&gt;172 sites let anyone delete records from the database without logging in. One DELETE request using the public key wipes entire tables.&lt;/p&gt;

&lt;p&gt;39 sites had tables fully readable by anyone holding the Supabase anon key, which is embedded in the page's JavaScript by design. So "holding it" just means "viewed source." Among those tables: &lt;code&gt;payments&lt;/code&gt;, &lt;code&gt;admin_users&lt;/code&gt;, and &lt;code&gt;chat_messages&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;308 sites exposed the anon key in JavaScript. On its own that's expected, it's a public key. The problem is what happens when no RLS policy exists behind it.&lt;/p&gt;

&lt;p&gt;34 sites had columns containing emails, hashed passwords, auth tokens, and phone numbers, all directly queryable through the Supabase REST API.&lt;/p&gt;

&lt;p&gt;None of this is theoretical. CVE-2025-48757 is the one that made headlines. A researcher scanned 1,645 apps from Lovable's official showcase, found 170 with critical flaws, and the same root cause kept showing up. Missing row-level security. MITRE scored it CVSS 9.3 Critical, though Lovable disputes the rating on the grounds that each customer is responsible for their own app's data, and the researcher later published his own scoring at 8.26. NVD lists the record as disputed. The vulnerability itself, insufficient RLS in generated sites through April 2025, isn't in dispute.&lt;/p&gt;

&lt;p&gt;Separately, a February 2026 report found an EdTech app with 16 vulnerabilities, 6 of them critical. 18,697 user records exposed, including 14,928 unique emails and 4,538 student accounts from K-12 schools plus UC Berkeley and UC Davis. The auth logic was inverted. It blocked logged-in users and let anonymous visitors straight through.&lt;/p&gt;

&lt;p&gt;Then there's Moltbook, an AI-agent social network. Wiz Research found zero RLS on any table. 1.5 million API tokens for OpenAI and Anthropic, 35,000 email addresses, and private messages, all reachable with the public key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this keeps happening
&lt;/h2&gt;

&lt;p&gt;Supabase turns row-level security off by default when you create tables through SQL. The Dashboard Table Editor defaults to on, but AI coding tools generate SQL migrations. They don't click through a Dashboard. So every table the AI creates starts unprotected.&lt;/p&gt;

&lt;p&gt;There's a documented pattern for what follows, called the fix loop. Palo Alto's Unit 42 described it. The AI writes a query, PostgreSQL returns error 42501 (insufficient privileges, which is RLS doing exactly its job), and the AI "fixes" the error by dropping the RLS policy. The query works. The table is now open. Nobody notices, because the app appears to function.&lt;/p&gt;

&lt;p&gt;Carnegie Mellon measured the underlying gap. 61% of AI-generated code is functionally correct, but only 10.5% is secure. Auth code is where that difference draws blood.&lt;/p&gt;

&lt;p&gt;It isn't only Supabase either. 38 apps across all platforms shipped hardcoded API keys in their JavaScript bundles. On Bolt.host, 17 of 251 apps. On Vercel's AI-generated apps, 18 of 67, which is 26.9%. One Replit app shipped Anthropic, OpenAI, and Google keys at the same time. Those keys bill per token. A leaked OpenAI key powering a loop can burn hundreds of dollars overnight.&lt;/p&gt;

&lt;p&gt;Security headers are missing almost everywhere. 93.6% of scanned apps had no Cross-Origin-Resource-Policy. 78.4% had no Content-Security-Policy. 70.6% had no X-Frame-Options.&lt;/p&gt;

&lt;p&gt;The common thread, as one report put it: AI code generators optimize for "does it work?" and not "is it safe?" Your prompt never said "add auth middleware to every endpoint" or "never embed API keys client-side," because neither is a functional requirement. The code works perfectly in a demo and fails catastrophically in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 17-point checklist
&lt;/h2&gt;

&lt;p&gt;Work through this list. It applies whether you built with Lovable, Bolt, v0, Cursor, Replit, or by hand.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Protect admin routes.&lt;/strong&gt; Every admin path needs an auth check on the server, not a hidden link or a client-side conditional. If the route renders, the route is reachable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Server-side permissions.&lt;/strong&gt; Authorization decisions belong on the server. Anything the browser enforces, the browser can edit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Enable RLS.&lt;/strong&gt; Run &lt;code&gt;ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;&lt;/code&gt; for every table in the public schema, then &lt;code&gt;ALTER TABLE your_table FORCE ROW LEVEL SECURITY;&lt;/code&gt; so policies can't be bypassed. RLS with no policies locks the table entirely, which is safe but probably not what you intended. Write policies scoped to the row's owner, not to "any authenticated user." Then &lt;strong&gt;verify as anon&lt;/strong&gt;, not as a logged-in test user. Most of the data-exposure findings above exist because the check was skipped for the anonymous public key, which is embedded in your page's JavaScript by design. One read is enough: &lt;code&gt;const supabase = createClient(url, anonKey); const { data } = await supabase.from('payments').select('*').limit(1);&lt;/code&gt; Returning rows before anyone signs in is the tripwire. It also catches the policy-exists-but-does-nothing case: &lt;code&gt;using (true)&lt;/code&gt; on a select passes an RLS-enabled scan while exposing every row to anon. Enabled is not the same as enforced.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify email addresses.&lt;/strong&gt; A confirmation flow that doesn't confirm anything is decoration. Turn it on and test it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hash passwords securely.&lt;/strong&gt; bcrypt, scrypt, or Argon2. Never SHA-256, never MD5, never plaintext.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep tokens out of local storage.&lt;/strong&gt; Any script on the page can read &lt;code&gt;localStorage&lt;/code&gt;. Session tokens belong in httpOnly, Secure, SameSite cookies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Server-side API secrets.&lt;/strong&gt; No keys in client code. Check your &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; and &lt;code&gt;VITE_&lt;/code&gt; variables specifically, because those prefixes mean "bundle this into the JavaScript that ships to every visitor." A &lt;code&gt;service_role&lt;/code&gt; key in client code bypasses every RLS policy you wrote.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Parameterized SQL queries.&lt;/strong&gt; Never concatenate user input into a query.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validate form inputs.&lt;/strong&gt; Server-side, with a schema. Client-side validation is a UX feature, not a security control.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Block cross-site scripting.&lt;/strong&gt; Escape output, avoid &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; with untrusted content, set a Content-Security-Policy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validate file uploads.&lt;/strong&gt; Check type and size, and never trust the filename. Don't serve uploads from the same origin as your app.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify webhook signatures.&lt;/strong&gt; An unverified webhook endpoint accepts forged events from anyone who guesses the URL.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rate limit requests.&lt;/strong&gt; Login, signup, password reset, and anything that sends email or costs money.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tighten CORS settings.&lt;/strong&gt; No wildcard origins on authenticated endpoints. Origin reflection with credentials enabled means any website can make authenticated requests on a logged-in user's behalf.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Disable production debugging.&lt;/strong&gt; Debug endpoints, verbose stack traces, GraphQL introspection, and source maps should all be off.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Update dependencies.&lt;/strong&gt; Run the audit, fix what it flags.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run an actual security review.&lt;/strong&gt; Anthropic open-sourced &lt;code&gt;claude-code-security-review&lt;/code&gt; under MIT. It's a GitHub Action that comments findings on your PRs, and Claude Code ships a &lt;code&gt;/security-review&lt;/code&gt; slash command that does the same analysis. One caveat worth knowing: the action is not hardened against prompt injection, and Anthropic's own README says to use it only on trusted PRs. Set your repo to "require approval for all external contributors" first.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Two things people get wrong
&lt;/h2&gt;

&lt;p&gt;The checklist is worth nothing if you skip these.&lt;/p&gt;

&lt;p&gt;Confirm your &lt;code&gt;.env&lt;/code&gt; files are actually excluded from Git, not just that they should be. Run &lt;code&gt;git check-ignore .env&lt;/code&gt; and make sure it returns a match. If a secret ever reached a commit, rotating it is the only real fix. Rewriting history is cleanup, not a cure.&lt;/p&gt;

&lt;p&gt;Then confirm sensitive data stays out of your logs. Logs get shipped, aggregated, and read by more people than you think. Never log tokens, passwords, full request bodies, or PII. Check your error handlers specifically, because that's where request objects get dumped wholesale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;Your app probably works. That's the part AI genuinely got good at. The security defaults underneath it are a different story, and the pattern across thousands of scanned apps says most of them haven't been touched.&lt;/p&gt;

&lt;p&gt;None of this is expensive or exotic to fix. RLS is a policy on a table. Keeping a key server-side is moving a file. A CSP header is a config line. A large share of the exposures found in these scans were one default away from not existing.&lt;/p&gt;

&lt;p&gt;Go check your tables. Start with the ones holding user data.&lt;/p&gt;

</description>
      <category>security</category>
      <category>supabase</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Billions of Log Rows, One Command to Make Them Disappear</title>
      <dc:creator>Chizee</dc:creator>
      <pubDate>Sun, 20 Sep 2026 10:07:56 +0000</pubDate>
      <link>https://dev.to/chizee/billions-of-log-rows-one-command-to-make-them-disappear-1pa4</link>
      <guid>https://dev.to/chizee/billions-of-log-rows-one-command-to-make-them-disappear-1pa4</guid>
      <description>&lt;p&gt;Telemetry and event logs only grow. Dump them into one giant table and every query — even one scoped to a single day — ends up scanning far more data than it needs to. That's usually the exact moment a team reaches for a dedicated time-series database like InfluxDB.&lt;/p&gt;

&lt;p&gt;Two native Postgres features cover most of what people reach for InfluxDB to get: declarative partitioning and BRIN indexes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split the table without splitting the app
&lt;/h2&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;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;bigserial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event_time&lt;/span&gt;  &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;     &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;RANGE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_2026_06&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;OF&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-06-01'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-07-01'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One note that will save you a confusing error: &lt;code&gt;id&lt;/code&gt; is deliberately not a primary key here. On a partitioned table, any unique or primary key constraint has to include the partition column. If you need one, it's &lt;code&gt;PRIMARY KEY (id, event_time)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A featherweight index built for this exact shape of data
&lt;/h2&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;idx_events_time_brin&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;BRIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_time&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;BRIN&lt;/code&gt; (Block Range Index) stores just the minimum and maximum value for each physical block of disk, instead of indexing every single row the way a B-Tree does. For data that arrives roughly in time order — which almost all event data does — that's enough to skip millions of irrelevant pages instantly, at a fraction of a B-Tree's size.&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;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event_time&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-10'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2026-06-11'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;EXPLAIN&lt;/code&gt; on that and you'll see the planner only touches the one relevant partition — the rest are pruned before the query even runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retention becomes a metadata operation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_2026_06&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No &lt;code&gt;DELETE&lt;/code&gt; scan, no bloat left behind to vacuum. Dropping a partition removes its rows instantly — I tested this directly: 500 seeded rows, one &lt;code&gt;DROP TABLE&lt;/code&gt;, zero rows left, no measurable delay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this beats a dedicated time-series database
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Retention policies are a &lt;code&gt;DROP TABLE&lt;/code&gt;, not a scheduled bulk delete job.&lt;/li&gt;
&lt;li&gt;BRIN indexes cost a sliver of what an equivalent B-Tree would.&lt;/li&gt;
&lt;li&gt;It's the same SQL as the rest of your schema — no separate query language, no separate client library.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where InfluxDB still wins
&lt;/h2&gt;

&lt;p&gt;Purpose-built downsampling and retention policies out of the box, at ingest rates in the millions of points per second. If you're managing your own partitions and indexes comfortably below that, you're not missing anything by staying on Postgres.&lt;/p&gt;




&lt;p&gt;This is one of eight infrastructure swaps in &lt;strong&gt;Just Use Postgres&lt;/strong&gt;, a 24-page field manual on replacing MongoDB, Redis, Elasticsearch, Pinecone, and more with the database you're probably already running. Every recipe in it — including this one — was run against a live Postgres instance before it went in the book.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Get the field manual&lt;/strong&gt; — launch price &lt;strong&gt;$14&lt;/strong&gt; instead of scanning a billion-row table for yesterday's data: &lt;a href="https://payhip.com/omenabyte" rel="noopener noreferrer"&gt;https://payhip.com/omenabyte&lt;/a&gt;&lt;br&gt;
🐳 Or take the &lt;strong&gt;$24 bundle&lt;/strong&gt; with the full &lt;code&gt;docker-compose up&lt;/code&gt; starter repo — all 8 modules as tested, runnable migrations + seed data: &lt;a href="https://4693433176360.gumroad.com/" rel="noopener noreferrer"&gt;https://4693433176360.gumroad.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Read this on omenabyte.com → &lt;a href="https://omenabyte.com/blog/replace-influxdb-with-partitioning-brin" rel="noopener noreferrer"&gt;https://omenabyte.com/blog/replace-influxdb-with-partitioning-brin&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>timeseries</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>You Don't Need MongoDB for "Flexible" Data. You Need One Column Type.</title>
      <dc:creator>Chizee</dc:creator>
      <pubDate>Sun, 06 Sep 2026 10:14:11 +0000</pubDate>
      <link>https://dev.to/chizee/you-dont-need-mongodb-for-flexible-data-you-need-one-column-type-35c1</link>
      <guid>https://dev.to/chizee/you-dont-need-mongodb-for-flexible-data-you-need-one-column-type-35c1</guid>
      <description>&lt;p&gt;Product catalogs, form submissions, webhook payloads — a lot of real data genuinely doesn't want a fixed schema. The usual response is reaching for MongoDB purely for that flexibility, which means a second connection pool, a second backup strategy, and a second place your data can quietly drift out of sync with the relational tables that still describe your users and orders.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jsonb&lt;/code&gt; solves the actual problem — flexible structure — without giving up the database you already trust for everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store anything
&lt;/h2&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;products&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;attributes&lt;/span&gt;  &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_products_attributes&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;jsonb&lt;/code&gt;, not &lt;code&gt;json&lt;/code&gt;. The binary form is stored decomposed at write time instead of as text re-parsed on every read — that's the difference between something you can actually index and something you can only display.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query it like you mean it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'{"color": "red", "size": "M"}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The GIN index makes this fast the same way a book's index works: it maps individual keys and values straight to the rows that contain them, instead of scanning every row to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part MongoDB genuinely can't do as cleanly
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt; &lt;span class="n"&gt;oi&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&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;oi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'specs'&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'material'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'leather'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a document-style attribute joined directly against relational order history, in one statement, inside one transaction. Try that across two databases and you're writing application-layer stitching code — and hoping nothing changes between the two calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this beats standing up a document database
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One transaction, not two systems — the "document" and the relational rows referencing it commit or roll back together.&lt;/li&gt;
&lt;li&gt;No sync jobs, because there's no second database to keep consistent.&lt;/li&gt;
&lt;li&gt;You can still join it, which is the thing you actually lose by going full-document.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where MongoDB still wins
&lt;/h2&gt;

&lt;p&gt;Genuinely schema-less workloads at a write scale where horizontal partitioning across many nodes is a day-one requirement, or built-in multi-region active-active replication out of the box. Most projects reaching for Mongo on day one aren't actually at that scale yet.&lt;/p&gt;




&lt;p&gt;This is one of eight infrastructure swaps in &lt;strong&gt;Just Use Postgres&lt;/strong&gt;, a 24-page field manual on replacing MongoDB, Redis, Elasticsearch, Pinecone, and more with the database you're probably already running. Every recipe in it — including this one — was run against a live Postgres instance before it went in the book.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Get the field manual&lt;/strong&gt; — launch price &lt;strong&gt;$14&lt;/strong&gt; instead of multiplying connection pools, backup jobs, and consistency headaches: &lt;a href="https://payhip.com/omenabyte" rel="noopener noreferrer"&gt;https://payhip.com/omenabyte&lt;/a&gt;&lt;br&gt;
🐳 Or take the &lt;strong&gt;$24 bundle&lt;/strong&gt; with the full &lt;code&gt;docker-compose up&lt;/code&gt; starter repo — all 8 modules as tested, runnable migrations + seed data: &lt;a href="https://4693433176360.gumroad.com/" rel="noopener noreferrer"&gt;https://4693433176360.gumroad.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Read this on omenabyte.com → &lt;a href="https://omenabyte.com/blog/replace-mongodb-with-jsonb" rel="noopener noreferrer"&gt;https://omenabyte.com/blog/replace-mongodb-with-jsonb&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>mongodb</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your AI Feature Doesn't Need Pinecone. It Needs pgvector.</title>
      <dc:creator>Chizee</dc:creator>
      <pubDate>Sun, 30 Aug 2026 22:50:42 +0000</pubDate>
      <link>https://dev.to/chizee/your-ai-feature-doesnt-need-pinecone-it-needs-pgvector-4e5k</link>
      <guid>https://dev.to/chizee/your-ai-feature-doesnt-need-pinecone-it-needs-pgvector-4e5k</guid>
      <description>&lt;h1&gt;
  
  
  Launch Workspace — "Just Use Postgres" Week 3 (Article 3 / Week 4)
&lt;/h1&gt;

&lt;p&gt;Dev.to title: Your AI Feature Doesn't Need Pinecone. It Needs pgvector.&lt;br&gt;
Slug: /blog/replace-pinecone-with-pgvector&lt;br&gt;
Canonical URL: &lt;a href="https://omenabyte.com/blog/replace-pinecone-with-pgvector" rel="noopener noreferrer"&gt;https://omenabyte.com/blog/replace-pinecone-with-pgvector&lt;/a&gt;&lt;br&gt;
dev.to tags: postgres, ai, vectordatabase, tutorial&lt;br&gt;
Cover: &lt;a href="https://omenabyte.com/blog/just-use-postgres-cover.png" rel="noopener noreferrer"&gt;https://omenabyte.com/blog/just-use-postgres-cover.png&lt;/a&gt;&lt;br&gt;
Series: The Field Manual Series&lt;br&gt;
Medium topics: &lt;code&gt;PostgreSQL, Databases, SQL, Web Development, Software Engineering&lt;/code&gt;&lt;/p&gt;


&lt;h1&gt;
  
  
  Your AI Feature Doesn't Need Pinecone. It Needs pgvector.
&lt;/h1&gt;

&lt;p&gt;The moment a project needs semantic search or RAG, the instinct is to bolt on a dedicated vector database. It works fine right up until you need a semantic match &lt;em&gt;and&lt;/em&gt; a relational filter at the same time — "find documents like this one, but only ones this specific user wrote" — and now you're querying two separate systems and stitching results back together over a network call.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pgvector&lt;/code&gt; puts the embedding in the same row as everything else about the thing it describes, so that query is just... a query.&lt;/p&gt;
&lt;h2&gt;
  
  
  The setup
&lt;/h2&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="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;author_id&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&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;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;content&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;embedding&lt;/span&gt;   &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_documents_embedding&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;HNSW&lt;/code&gt; (Hierarchical Navigable Small World) builds a multi-layered graph over your vectors — think of it as a high-dimensional skip list — so approximate nearest-neighbor search stays fast as the table grows.&lt;/p&gt;
&lt;h2&gt;
  
  
  The query that used to need two databases
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;author_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'[0.012, -0.045, 0.031, ...]'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Semantic ranking and a relational filter, one round trip, one transaction, one system to keep consistent.&lt;/p&gt;
&lt;h2&gt;
  
  
  The gotcha nobody mentions
&lt;/h2&gt;

&lt;p&gt;Here's something worth knowing before you ship this: with an approximate index like HNSW, Postgres fetches the nearest candidates &lt;em&gt;first&lt;/em&gt;, then applies the &lt;code&gt;WHERE&lt;/code&gt; clause &lt;em&gt;after&lt;/em&gt;. If &lt;code&gt;author_id = 42&lt;/code&gt; is a narrow slice of a large table, you can get back fewer than 5 rows — not an error, just a quietly short result.&lt;/p&gt;

&lt;p&gt;The fix, if you're on pgvector 0.8 or newer:&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;SET&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iterative_scan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'strict_order'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps scanning until the filter is actually satisfied instead of settling for whatever the first pass turned up. Worth checking your version — &lt;code&gt;SELECT extversion FROM pg_extension WHERE extname = 'vector';&lt;/code&gt; — since some package managers (looking at you, plain &lt;code&gt;apt&lt;/code&gt;) ship versions old enough not to have this option yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this beats a dedicated vector database for most teams
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Embeddings and the rows they describe are written in the same transaction — they can never drift out of sync.&lt;/li&gt;
&lt;li&gt;Hybrid search (semantic + relational) is a single query instead of an application-layer join.&lt;/li&gt;
&lt;li&gt;One fewer vendor, one fewer bill.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Pinecone still wins
&lt;/h2&gt;

&lt;p&gt;Billion-vector scale, with dedicated horizontally-sharded ANN infrastructure and managed elastic scaling. If you're not there yet, you're paying for infrastructure you don't need.&lt;/p&gt;




&lt;p&gt;This is one of eight infrastructure swaps in &lt;strong&gt;Just Use Postgres&lt;/strong&gt;, a 24-page field manual on replacing MongoDB, Redis, Elasticsearch, Pinecone, and more with the database you're probably already running. Every recipe in it — including this one — was run against a live Postgres instance with &lt;code&gt;pgvector&lt;/code&gt; before it went in the book.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://payhip.com/omenabyte" rel="noopener noreferrer"&gt;Get the field manual&lt;/a&gt; — launch price &lt;strong&gt;$14&lt;/strong&gt; instead of walking into a $50/month managed-vector invoice&lt;/p&gt;

&lt;p&gt;🐳 Or take the &lt;strong&gt;$24 bundle&lt;/strong&gt; with the full &lt;code&gt;docker-compose up&lt;/code&gt; starter repo — all 8 modules as tested, runnable migrations + seed data: &lt;a href="https://4693433176360.gumroad.com/" rel="noopener noreferrer"&gt;https://4693433176360.gumroad.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Read this on omenabyte.com → &lt;a href="https://omenabyte.com/blog/replace-pinecone-with-pgvector" rel="noopener noreferrer"&gt;https://omenabyte.com/blog/replace-pinecone-with-pgvector&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>ai</category>
      <category>vectordatabase</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Watched a Man's Certainty Evaporate in Real Time</title>
      <dc:creator>Chizee</dc:creator>
      <pubDate>Sun, 30 Aug 2026 09:51:37 +0000</pubDate>
      <link>https://dev.to/chizee/i-watched-a-mans-certainty-evaporate-in-real-time-4lm3</link>
      <guid>https://dev.to/chizee/i-watched-a-mans-certainty-evaporate-in-real-time-4lm3</guid>
      <description>&lt;h2&gt;
  
  
  The most dangerous hacker in the room never touched a keyboard.
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Or: why "I use strong passwords" is the least effective security strategy you own.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;There's a story we tell ourselves about security. It goes like this: &lt;em&gt;as long as I use strong, unique passwords, enable two-factor authentication, and never click on anything even slightly suspicious, I'm one of the safe ones.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I told myself that story for years. Then I watched a woman break it apart in real time — through a screen, on someone else's podcast — and it rattled me more than I expected it to.&lt;/p&gt;

&lt;p&gt;The man in the hot seat ran a security-focused YouTube channel and podcast. He'd built his entire platform on exposing scammers and teaching everyday people how to outsmart them. Two-factor authentication? On. Passwords? Long, random, different on every damn site. Sketchy links? Never touched them. By every checklist the internet had ever handed him, he was bulletproof.&lt;/p&gt;

&lt;p&gt;He sat down across from &lt;strong&gt;Rachel Tobac&lt;/strong&gt; — CEO of Social Proof Security, one of the best social engineers on the planet — and offered her a challenge: &lt;em&gt;research me. Try to break me.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few sentences later, the smug was gone.&lt;/p&gt;

&lt;p&gt;She said his &lt;strong&gt;full legal name&lt;/strong&gt; — bleeped for the audio, the name he'd gone out of his way for years to never use publicly. Then she told him where he was from. Then she named the city. Then she started pulling out the strange, buried details of an old life — a competitive program he'd almost forgotten, a hobby he'd packed away years ago, even an &lt;em&gt;America's Got Talent&lt;/em&gt; audition that never made it to air.&lt;/p&gt;

&lt;p&gt;He stopped looking her in the eye. His hands went cold.&lt;/p&gt;

&lt;p&gt;She hadn't hacked his computer. She hadn't cracked a single password. She hadn't touched a single line of code. She had done something far more dangerous: &lt;strong&gt;she had hacked him.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The uncomfortable truth: you're the weakest link
&lt;/h2&gt;

&lt;p&gt;Social engineering isn't hacking computers. It's hacking the operating system that runs them: &lt;em&gt;you.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And here's what decades of data breaches, security research, and live demonstrations keep proving — the strongest technical defense in the world becomes meaningless the moment a stranger can convince a human being to open the door:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Everyone can be hacked except you. You've got two-factor authentication turned on. All of your passwords are super strong. … But you're still not safe from this person." — Rachel Tobac&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The attacks that actually work aren't glamorous. They don't involve servers or exploit chains. They're:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A 30-second phone call.&lt;/strong&gt; Rachel's first professional engagement: she called an executive's assistant, pretended to be from finance, and got everything she needed — the kind of information that could move company money — in &lt;em&gt;thirty seconds&lt;/em&gt;. She didn't attack the gatekeeper's boss. She attacked the person whose job is to be agreeable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MFA fatigue.&lt;/strong&gt; Attackers don't break your two-factor authentication. They &lt;em&gt;spam&lt;/em&gt; it — push notification after push notification at 11pm, until you're tired and annoyed and just want it to stop. Then you hit &lt;em&gt;accept&lt;/em&gt;, and the game is over. Around half of people admit to reusing passwords (Google's survey), which is what makes the spam worth sending.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caller ID that lies.&lt;/strong&gt; Your phone displays a name and photo for numbers in your contacts. Attackers can make a call &lt;em&gt;display&lt;/em&gt; as if it's from your bank — or your mom. In the interview, Rachel did something she'd never done on air: she called the host live, by phone, in front of his co-host and millions of listeners. He &lt;em&gt;knew&lt;/em&gt; it was happening, in real time, and still almost went for it. His words afterward: &lt;strong&gt;"I'm still reeling from that."&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scariest part isn't that an expert got caught. It's that he wasn't special. The words that broke him would work on almost anyone in that room.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 10 rules of being politely paranoid
&lt;/h2&gt;

&lt;p&gt;The good news: if the game is two steps — &lt;em&gt;build rapport, make a plausible ask&lt;/em&gt; — then the defense can be taught in two steps too. When Rachel was asked for her final advice, she gave a phrase that's worth writing on a sticky note:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Be politely paranoid — because the information that's out there for almost every person on the internet can be used to trick you, or the people around you."&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here are the ten rules distilled from that conversation. Screenshot this. Read it twice. Then teach it to your parents — scammers target them through their own kindness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Alerts you didn't start are always suspicious.&lt;/strong&gt; If a login push arrives and you didn't just log in — that's your first red flag. Don't accept, don't deny, don't trust anyone on the phone who insists you do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Verify through a second channel — always.&lt;/strong&gt; Anyone asking you to move money, reset access, or share a code gets one response: a fresh conversation on a channel &lt;em&gt;you&lt;/em&gt; control. Call the number on the back of your card. A real request survives the check. A scam never does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Slow down the moment someone creates urgency.&lt;/strong&gt; "It has to be now" is a script, not a fact. Real systems don't panic you. Scammers do. Your superpower is refusing to rush.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Treat caller ID as a suggestion, not proof.&lt;/strong&gt; Numbers and contact cards can be faked. If a call looks like your bank and starts asking for anything — verify on a number you dialed yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. One unique password per account.&lt;/strong&gt; A breach at some forgotten 2016 site should cost you exactly one account — never the keys to your bank, your email, and your work. A password manager makes this painless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. MFA on, but never MFA by panic.&lt;/strong&gt; Keep two-factor authentication everywhere. Just remember its one weakness: the accept button. Treat every prompt you didn't start as suspicious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Be stingy with your face and your ID.&lt;/strong&gt; You can rotate a password. You can cancel a card. &lt;strong&gt;You cannot change your face.&lt;/strong&gt; Hand out biometrics and government ID as if they were the last copies on earth — because they effectively are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Treat AI output like a stranger on the phone.&lt;/strong&gt; Plausible doesn't mean true. Rachel has documented cases where AI &lt;em&gt;reinforced&lt;/em&gt; people's delusions instead of correcting them — an agreeable machine that never tells you you're wrong. Verify anything important it tells you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Be the wall for people around you.&lt;/strong&gt; Scammers target your family and colleagues through their own kindness. Be the person who gently asks, &lt;em&gt;"wait — did you actually ask for this?"&lt;/em&gt; before someone loses years of savings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. Keep playing whack-a-mole.&lt;/strong&gt; Security is a habit, not a finish line. New scams will keep arriving. Don't tune out — spot the current one, defend, teach, move on. That's the whole game, and you can win it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the machines make it worse (and better)
&lt;/h2&gt;

&lt;p&gt;Rachel's warnings about AI are the most urgent part of the conversation. There's a real case of a well-known venture capitalist who spiraled into a false belief system — convinced he was being tracked by a shadowy organization — because the LLM he talked to validated his delusions, drawing on science fiction in its training data and presenting it as fact. No attacker involved. The danger was the tool itself: an agreeable machine that could not tell him he was wrong.&lt;/p&gt;

&lt;p&gt;But there's a counterweight. The same technology that writes a perfect phishing email can catch the worst content on the internet so no human has to look at it. Rachel once interviewed for a job moderating content for a major social platform — the interviewer asked how comfortable she'd be seeing pictures of, say, &lt;em&gt;children in cages&lt;/em&gt;. She didn't get the job, and she's glad. Some jobs are too horrible for humans, and those are the jobs the machines should do.&lt;/p&gt;

&lt;p&gt;The honest forecast for AI security is roughly 50/50 — a perpetual whack-a-mole between offense and defense. The side that wins is the side that stays sharp.&lt;/p&gt;




&lt;h2&gt;
  
  
  The strongest password you'll ever have
&lt;/h2&gt;

&lt;p&gt;Here's the line that closed the conversation, and it's worth sitting with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The strongest password you'll ever have is the refusal to believe the last person who asked.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The scammers are praying you don't know what scams look like. They bank on urgency, pressure, and a phone number that looks exactly right. When you're politely paranoid — warm, human, but unwilling to act on urgency alone — you catch them every time.&lt;/p&gt;

&lt;p&gt;I went so deep into this conversation — the 30-second hack, the MFA fatigue game, the live call, the AI psychosis cases, and the full defense playbook — that I turned it into a book. It's called &lt;strong&gt;Be Politely Paranoid&lt;/strong&gt;, and it's the complete, chapter-by-chapter breakdown of how social engineers think and how to build your human firewall.&lt;/p&gt;

&lt;p&gt;If this post resonated, the book goes deeper — every attack dissected, every defense explained, and the full 10-rule playbook you can print and stick on your wall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://4693433176360.gumroad.com/l/be-politely-paranoid" rel="noopener noreferrer"&gt;Get &lt;em&gt;Be Politely Paranoid&lt;/em&gt; here&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It's the cheapest security upgrade you'll ever buy — and it protects the people you love, not just your accounts.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is an independent commentary on a publicly available interview. It is not authorized, endorsed, or reviewed by Rachel Tobac or Social Proof Security. Quotes have been lightly edited for readability.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>security</category>
      <category>cybersecurity</category>
      <category>socialeng</category>
      <category>infosec</category>
    </item>
  </channel>
</rss>
