<?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: Mike Clarke</title>
    <description>The latest articles on DEV Community by Mike Clarke (@mike_clarke_50a95013f5c59).</description>
    <link>https://dev.to/mike_clarke_50a95013f5c59</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%2F3868865%2Ff368d9be-f55c-4ab9-a26e-a73625709b2b.jpg</url>
      <title>DEV Community: Mike Clarke</title>
      <link>https://dev.to/mike_clarke_50a95013f5c59</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mike_clarke_50a95013f5c59"/>
    <language>en</language>
    <item>
      <title>The table looked general-purpose. The schema disagreed.</title>
      <dc:creator>Mike Clarke</dc:creator>
      <pubDate>Tue, 21 Jul 2026 14:00:09 +0000</pubDate>
      <link>https://dev.to/mike_clarke_50a95013f5c59/the-table-looked-general-purpose-the-schema-disagreed-9h9</link>
      <guid>https://dev.to/mike_clarke_50a95013f5c59/the-table-looked-general-purpose-the-schema-disagreed-9h9</guid>
      <description>&lt;p&gt;A CHECK constraint is documentation with teeth.&lt;/p&gt;

&lt;p&gt;That's the whole lesson. But here's what it looks like when you learn it at runtime instead of at read-time.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A CHECK constraint encodes scope assumptions the column name never will&lt;/li&gt;
&lt;li&gt;A constraint violation inside a trigger aborts the parent transaction — silently, from the caller's perspective&lt;/li&gt;
&lt;li&gt;Before writing to a table you inherited, run &lt;code&gt;\d tablename&lt;/code&gt; or query &lt;code&gt;information_schema.check_constraints&lt;/code&gt;. Read what's there.&lt;/li&gt;
&lt;li&gt;Gate on allowed values in code before the insert. Let the constraint be a backstop, not the first line of defense.&lt;/li&gt;
&lt;li&gt;Skip-and-log beats throw-and-abort when the parent write is more important than the child record&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;We run ARIA, an autonomous CRM and nurture automation system at Elevare Digital. New contact records enroll into follow-up sequences automatically — no human queues the work.&lt;/p&gt;

&lt;p&gt;At some point, new records stopped enrolling. The parent write (creating the contact) was aborting entirely. No sequence. No contact. No error surfaced to the caller in a useful way.&lt;/p&gt;

&lt;p&gt;The culprit was a &lt;code&gt;region&lt;/code&gt; column with a CHECK constraint that looked roughly like this:&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;enrollment_tracker&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;region_allowed&lt;/span&gt;
  &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'north'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'south'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'east'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'west'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'central'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The table had been built for one specific program covering five regions. Later, code started treating it as a general enrollment tracker and writing region values the constraint never anticipated. Postgres rejected the insert. Because that insert happened inside a trigger, the whole parent transaction rolled back.&lt;/p&gt;

&lt;p&gt;The column was named &lt;code&gt;region&lt;/code&gt;. Nothing in that name says "only these five values are valid." The constraint said it. Nobody read the constraint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a trigger makes this worse
&lt;/h2&gt;

&lt;p&gt;If you insert directly into a table and violate a CHECK, you get an error back immediately. Annoying, but contained.&lt;/p&gt;

&lt;p&gt;When the insert is inside a trigger on a &lt;em&gt;different&lt;/em&gt; table, the error propagates up and aborts the statement that fired the trigger. The caller sees their write fail. They may have no idea a trigger was involved, let alone which constraint fired inside it.&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="c1"&gt;-- Trigger fires on INSERT to contacts&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;enroll_contact&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="c1"&gt;-- This insert can blow up the parent INSERT INTO contacts&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;enrollment_tracker&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enrolled_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&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="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&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;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&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;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;trg_enroll&lt;/span&gt;
&lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;contacts&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;enroll_contact&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now do this:&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;contacts&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'Acme Corp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'southeast'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- ERROR: new row for relation "enrollment_tracker" violates&lt;/span&gt;
&lt;span class="c1"&gt;-- check constraint "region_allowed"&lt;/span&gt;
&lt;span class="c1"&gt;-- DETAIL: Failing row contains (..., southeast, ...).&lt;/span&gt;
&lt;span class="c1"&gt;-- The contact was NOT created.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;southeast&lt;/code&gt; is a perfectly valid business concept. The table just never knew about it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix: gate before you insert
&lt;/h2&gt;

&lt;p&gt;Once we understood the constraint, the fix was straightforward. Check the allowed values in code (or in the trigger function itself) before attempting the insert. If the value isn't allowed, skip the enrollment record and log it. The parent write completes.&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;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;enroll_contact&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
  &lt;span class="n"&gt;allowed_regions&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'north'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'south'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'east'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'west'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'central'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;allowed_regions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;enrollment_tracker&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enrolled_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&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="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&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;ELSE&lt;/span&gt;
    &lt;span class="c1"&gt;-- Log it; don't abort the parent write&lt;/span&gt;
    &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;enrollment_skipped&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skipped_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&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="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'region not in enrollment_tracker allowed list'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternately, you can query the constraint definition directly rather than hardcoding the list — which is useful if the allowed values might expand:&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="c1"&gt;-- Pull allowed values from the constraint definition at runtime&lt;/span&gt;
&lt;span class="c1"&gt;-- (useful for visibility; hardcoding is fine if values are stable)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;consrc&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_constraint&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;conname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'region_allowed'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;conrelid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'enrollment_tracker'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns something like &lt;code&gt;(region = ANY (ARRAY['north'::text, 'south'::text, ...]))&lt;/code&gt;. Not the cleanest parse, but it tells you exactly what the schema intended.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to read the constraints you inherited
&lt;/h2&gt;

&lt;p&gt;Before writing to any table you didn't build:&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="c1"&gt;-- psql shortcut&lt;/span&gt;
&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;enrollment_tracker&lt;/span&gt;

&lt;span class="c1"&gt;-- Or query directly&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constraint_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_clause&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_constraints&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_constraints&lt;/span&gt; &lt;span class="n"&gt;cc&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;constraint_name&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'enrollment_tracker'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Constraints you'll find this way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CHECK&lt;/code&gt; — allowed values, ranges, cross-column rules&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UNIQUE&lt;/code&gt; — uniqueness you may not have assumed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NOT NULL&lt;/code&gt; — columns the schema considers required&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FOREIGN KEY&lt;/code&gt; — referential dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is hidden. It's just rarely read.&lt;/p&gt;




&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;The table name was &lt;code&gt;enrollment_tracker&lt;/code&gt;. That sounds general. It wasn't — it was built for a specific program with five regions, and the CHECK constraint was the only place that scope was written down.&lt;/p&gt;

&lt;p&gt;When later code treated it as a general tracker, it imported an assumption it never knew was there. The schema surfaced that assumption at write time, inside a trigger, in a way that took down the parent record.&lt;/p&gt;

&lt;p&gt;Schema constraints are the closest thing to binding documentation that most databases have. They don't drift. They don't get outdated and left in a wiki. They're enforced.&lt;/p&gt;

&lt;p&gt;Read them before you write. Not after.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;— Mike Clarke, founder of Elevare Digital.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>coding</category>
      <category>webdev</category>
      <category>learning</category>
    </item>
    <item>
      <title>Your AI agent checked its queue, found nothing, and went back to sleep. The queue was full.</title>
      <dc:creator>Mike Clarke</dc:creator>
      <pubDate>Fri, 17 Jul 2026 14:00:09 +0000</pubDate>
      <link>https://dev.to/mike_clarke_50a95013f5c59/your-ai-agent-checked-its-queue-found-nothing-and-went-back-to-sleep-the-queue-was-full-1m4</link>
      <guid>https://dev.to/mike_clarke_50a95013f5c59/your-ai-agent-checked-its-queue-found-nothing-and-went-back-to-sleep-the-queue-was-full-1m4</guid>
      <description>&lt;p&gt;Postgres Row-Level Security doesn't raise an error when it blocks you. It returns zero rows. Your query succeeds. Your agent sees an empty queue. Your agent idles. Your jobs pile up.&lt;/p&gt;

&lt;p&gt;This is what happened to ARIA, our autonomous AI system at Elevare Digital.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Postgres RLS silently filters rows on &lt;code&gt;SELECT&lt;/code&gt; — a blocked read and a genuinely empty table look identical to the caller&lt;/li&gt;
&lt;li&gt;An orchestrator that reads its own queue gets no exception, no warning, no non-200 status code when RLS blocks it&lt;/li&gt;
&lt;li&gt;A healthy heartbeat on an idle agent tells you nothing about whether the idle is real&lt;/li&gt;
&lt;li&gt;The fix: add a canary count that verifies you &lt;em&gt;can&lt;/em&gt; read, not just that you &lt;em&gt;did&lt;/em&gt; read&lt;/li&gt;
&lt;li&gt;Treat an empty read as a question, not an answer&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What the agent saw
&lt;/h2&gt;

&lt;p&gt;The orchestrator polls a work queue table. Normal operation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Query for pending jobs&lt;/li&gt;
&lt;li&gt;If results → process them&lt;/li&gt;
&lt;li&gt;If empty → log idle heartbeat, sleep&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The logs showed healthy idle heartbeats. Monitoring showed the agent alive and polling. From the outside, everything looked fine.&lt;/p&gt;

&lt;p&gt;Jobs were not being processed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;Someone added a Row-Level Security policy to the queue table, scoped to &lt;code&gt;auth.uid()&lt;/code&gt;. Correct for user-facing reads. But the orchestrator connects under the service role — and no service-role bypass was added to the policy.&lt;/p&gt;

&lt;p&gt;Here's the policy as it was written:&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="c1"&gt;-- Added for user-facing queue visibility&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_own_jobs"&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;work_queue&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&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And RLS was enabled on the table:&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;work_queue&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No bypass for the service role. No &lt;code&gt;FOR ALL&lt;/code&gt; escape. The orchestrator's &lt;code&gt;SELECT&lt;/code&gt; now matched zero rows — because RLS filtered every row out before returning results.&lt;/p&gt;

&lt;p&gt;Postgres does not raise. It does not warn. The query completes with status 200 and an empty array.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Orchestrator poll — looks completely normal&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;work_queue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// error is null. jobs is []. Agent concludes: nothing to do.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;recordIdleHeartbeat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;error&lt;/code&gt; is null. The &lt;code&gt;jobs&lt;/code&gt; array is empty. The agent's logic is correct for the data it received. The data it received was wrong in a way that produced no signal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is hard to catch
&lt;/h2&gt;

&lt;p&gt;If RLS blocks a write, you often get an error — the row you tried to insert violates a policy, or returns nothing when you expected a rowcount. Writes are easier to notice.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SELECT&lt;/code&gt; under RLS is different. The design intent is that users should not be able to distinguish "this row doesn't exist" from "this row exists but you can't see it." That's a security feature. It's also exactly the wrong behavior for an orchestrator that needs to know the difference between "queue is empty" and "I am blind."&lt;/p&gt;

&lt;p&gt;The service role in Supabase bypasses RLS by default — but only if you're using the service-role key on the client. If your edge function or backend is using the anon key, or if it's operating in a context where &lt;code&gt;auth.uid()&lt;/code&gt; resolves to null, RLS applies and silently filters.&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="c1"&gt;-- This is what the orchestrator needed, but wasn't there&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;"service_role_bypass"&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;work_queue&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;ALL&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&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'service_role'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Or, simpler: grant the service role explicit bypass at the table level&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;work_queue&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;span class="c1"&gt;-- applies to all&lt;/span&gt;
&lt;span class="c1"&gt;-- and then in your Supabase client: use the service-role key, which bypasses RLS automatically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual bypass mechanism in Supabase is that the service-role JWT includes &lt;code&gt;"role": "service_role"&lt;/code&gt;, and Supabase's Postgres config grants that role &lt;code&gt;BYPASSRLS&lt;/code&gt;. If your client is initialized with the service-role key, you're fine. If it's not, RLS applies — no error, just silence.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix: a canary count
&lt;/h2&gt;

&lt;p&gt;The real repair has two parts: fix the RLS policy, and add a check that will catch this failure mode again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1 — Fix the policy:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Preserve user-facing policy&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_own_jobs"&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;work_queue&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&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Add explicit service-role access&lt;/span&gt;
&lt;span class="c1"&gt;-- (Or: initialize your orchestrator client with the service-role key,&lt;/span&gt;
&lt;span class="c1"&gt;--  which bypasses RLS automatically via BYPASSRLS grant)&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;"orchestrator_full_access"&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;work_queue&lt;/span&gt;
  &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;ALL&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&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'service_role'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;role&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'service_role'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Part 2 — The canary check:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The orchestrator now runs a canary query before trusting an empty result. The canary queries a row it knows exists — a sentinel row inserted specifically for this purpose, or a count from an unrestricted table the orchestrator owns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pollQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;queueError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;work_queue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queueError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;alertOpsChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;queue_poll_error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;queueError&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Jobs came back — process normally&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Empty result — but is it genuinely empty, or are we blind?&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canaryOk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyReadAccess&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;canaryOk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// This is the case we were missing before&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;alertOpsChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;queue_read_access_lost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Orchestrator received empty queue result but canary check failed. Possible RLS or permission regression.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Do NOT record idle heartbeat — we don't know the real state&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Canary passed — empty really means empty&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;recordIdleHeartbeat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyReadAccess&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Query a sentinel row that always exists in a known state&lt;/span&gt;
  &lt;span class="c1"&gt;// This could be a dedicated canary table, or a system-health row&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orchestrator_canary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heartbeat-sentinel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// We expected exactly one row. Getting nothing means access is broken.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The canary table is simple:&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;orchestrator_canary&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="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="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;orchestrator_canary&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="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'heartbeat-sentinel'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- No RLS on this table — it exists only so the orchestrator can verify it can read&lt;/span&gt;
&lt;span class="c1"&gt;-- If you want RLS: add only a service-role policy, nothing user-facing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the orchestrator has three states instead of two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jobs found → process&lt;/li&gt;
&lt;li&gt;No jobs, canary ok → genuinely idle&lt;/li&gt;
&lt;li&gt;No jobs, canary failed → alert, do not idle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third state was always real. We just had no way to observe it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The deeper issue with autonomous agents and silent failures
&lt;/h2&gt;

&lt;p&gt;Human-in-the-loop systems fail loudly. A user tries to load their queue, sees nothing, and files a ticket. An autonomous agent has no user. It reads, decides, acts. If the read is silently wrong, the decision is wrong, and nothing complains.&lt;/p&gt;

&lt;p&gt;This failure mode matters more as systems get more autonomous. ARIA processes queued work without someone watching every poll cycle. The assumption baked into the polling loop was: &lt;em&gt;an empty read means there is nothing to do.&lt;/em&gt; That assumption held until it didn't, and the system had no way to question it.&lt;/p&gt;

&lt;p&gt;The fix is to make the orchestrator skeptical of its own empty results. Not paranoid — just skeptical enough to verify the precondition that makes "empty" meaningful.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to check in your own system
&lt;/h2&gt;

&lt;p&gt;If you're running any kind of queue worker against Postgres or Supabase:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check which key your worker is using.&lt;/strong&gt; Supabase service-role key bypasses RLS. Anon key does not. Confirm which one is in your edge function or backend environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;List your RLS policies and check for missing bypasses.&lt;/strong&gt; &lt;code&gt;SELECT * FROM pg_policies WHERE tablename = 'your_queue_table';&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Look at your idle heartbeats.&lt;/strong&gt; If idle logging increased around the time you last modified permissions or added RLS, that's worth investigating.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add a canary.&lt;/strong&gt; Takes an hour. Catches this entire class of problem permanently.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;An empty queue result is not evidence of an empty queue. It's evidence that the query ran. Those are different things, and in autonomous systems, the difference matters.&lt;/p&gt;

&lt;p&gt;— Mike Clarke, founder of Elevare Digital.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>coding</category>
      <category>webdev</category>
      <category>learning</category>
    </item>
    <item>
      <title>The function was deployed, healthy, and never ran once</title>
      <dc:creator>Mike Clarke</dc:creator>
      <pubDate>Tue, 14 Jul 2026 14:00:05 +0000</pubDate>
      <link>https://dev.to/mike_clarke_50a95013f5c59/the-function-was-deployed-healthy-and-never-ran-once-1hgd</link>
      <guid>https://dev.to/mike_clarke_50a95013f5c59/the-function-was-deployed-healthy-and-never-ran-once-1hgd</guid>
      <description>&lt;p&gt;There's a category of bug where nothing is broken. The dashboard is green. The deployment succeeded. The function shows a recent update timestamp. And it has done zero work since you shipped it.&lt;/p&gt;

&lt;p&gt;We hit this in ARIA, our autonomous agent system. A scheduled function was silently doing nothing — not erroring, not timing out, just... absent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Supabase edge function can be perfectly deployed and still never execute&lt;/li&gt;
&lt;li&gt;JWT verification happens at the gateway, before your handler runs&lt;/li&gt;
&lt;li&gt;Gateway-level 401s produce no application logs, no error rows, no telemetry inside the function&lt;/li&gt;
&lt;li&gt;The only signal is the absence of invocation-log entries&lt;/li&gt;
&lt;li&gt;Functions called by &lt;code&gt;pg_cron&lt;/code&gt; need &lt;code&gt;verify_jwt=false&lt;/code&gt; if they authenticate via service-role key in the body&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What we saw
&lt;/h2&gt;

&lt;p&gt;ARIA runs a multi-agent scheduler. Several edge functions are invoked on a schedule via &lt;code&gt;pg_cron&lt;/code&gt;. One of them showed up in the Supabase dashboard as deployed, healthy, recently updated. We had no alerts firing.&lt;/p&gt;

&lt;p&gt;What we didn't have: any invocation log rows. Not failed ones. Not slow ones. Zero rows.&lt;/p&gt;

&lt;p&gt;Everything looked fine. Nothing was running.&lt;/p&gt;




&lt;h2&gt;
  
  
  The mechanism
&lt;/h2&gt;

&lt;p&gt;Supabase edge functions have a &lt;code&gt;verify_jwt&lt;/code&gt; flag. When it's set to &lt;code&gt;true&lt;/code&gt; (the default), the gateway validates the bearer token on every request before the function body executes.&lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;pg_cron&lt;/code&gt; job was calling the function with a service-role bearer token — correct credentials, correct format. But the gateway's JWT verification was rejecting it and returning a &lt;code&gt;401&lt;/code&gt; before the handler ever ran.&lt;/p&gt;

&lt;p&gt;The request never reached the function. The function had nothing to log.&lt;/p&gt;

&lt;p&gt;Here's what the pg_cron invocation looks like:&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;'run-agent-task'&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="err"&gt;$$&lt;/span&gt;
  &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://&amp;lt;project&amp;gt;.supabase.co/functions/v1/agent-task'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s1"&gt;'Content-Type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'Authorization'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Bearer '&lt;/span&gt; &lt;span class="o"&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;'app.service_role_key'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&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;jsonb&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the function config that caused the silent failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# supabase/functions/agent-task/config.toml&lt;/span&gt;
&lt;span class="nn"&gt;[functions.agent-task]&lt;/span&gt;
&lt;span class="py"&gt;verify_jwt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c"&gt;# &amp;lt;-- gateway rejects the cron call here, handler never runs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[functions.agent-task]&lt;/span&gt;
&lt;span class="py"&gt;verify_jwt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;  &lt;span class="c"&gt;# gateway passes the request through; function handles auth itself&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;verify_jwt=false&lt;/code&gt;, you move the authentication check inside the function body, where you can actually see what happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;serve&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://deno.land/std@0.168.0/http/server.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Validate against your service role key inside the handler&lt;/span&gt;
  &lt;span class="c1"&gt;// where failures are visible in your logs&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized request to agent-task&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// actual work happens here&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent-task executing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&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;Now when auth fails, you get a log line. You get an invocation row. You get something to look at.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is hard to catch
&lt;/h2&gt;

&lt;p&gt;The normal debugging loop assumes that if something failed, there's a record of the failure. You look at logs, you find the error, you fix it.&lt;/p&gt;

&lt;p&gt;Gateway-level rejections break that loop. The 401 happens in infrastructure you don't control and don't have direct log access to. From the function's perspective, the request never arrived. From the cron job's perspective, it fired successfully — &lt;code&gt;net.http_post&lt;/code&gt; queued the request and moved on.&lt;/p&gt;

&lt;p&gt;The only diagnostic signal is the shape of what's missing: no invocation rows in the function's log table, for a function that should be running every minute.&lt;/p&gt;

&lt;p&gt;If you're not actively checking for expected invocations, you won't notice.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we added after
&lt;/h2&gt;

&lt;p&gt;Beyond fixing the flag, we added a simple liveness check: a separate monitor queries the invocation log and alerts if a scheduled function hasn't produced a row in longer than two of its scheduled intervals. Not sophisticated, but it catches the "deployed and doing nothing" class of failure.&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="c1"&gt;-- Example: check that agent-task has run in the last 3 minutes&lt;/span&gt;
&lt;span class="c1"&gt;-- (for a function scheduled every minute)&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="k"&gt;max&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="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'3 minutes'&lt;/span&gt;
    &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="s1"&gt;'STALLED'&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s1"&gt;'OK'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;function_invocation_log&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;function_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'agent-task'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The specific table structure depends on how you surface invocation logs in your setup — the point is to query for expected evidence of execution, not just absence of errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  The thing worth internalizing
&lt;/h2&gt;

&lt;p&gt;Deployment success and runtime success are different things. A function can pass every health check and still never execute a single line of your code.&lt;/p&gt;

&lt;p&gt;For any function invoked by infrastructure (cron, queues, webhooks) rather than by a user, you need positive confirmation of execution — not just the absence of errors. Absence of errors and absence of execution look identical from the outside.&lt;/p&gt;

&lt;p&gt;Check your cron-invoked functions. If &lt;code&gt;verify_jwt=true&lt;/code&gt; and you're calling with a service-role token, check the invocation log. The rows might not be there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;— Mike Clarke, founder of Elevare Digital.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>coding</category>
      <category>webdev</category>
      <category>learning</category>
    </item>
    <item>
      <title>Your cron logged success every day for weeks. It approved nothing.</title>
      <dc:creator>Mike Clarke</dc:creator>
      <pubDate>Sun, 12 Jul 2026 00:10:11 +0000</pubDate>
      <link>https://dev.to/mike_clarke_50a95013f5c59/your-cron-logged-success-every-day-for-weeks-it-approved-nothing-1llg</link>
      <guid>https://dev.to/mike_clarke_50a95013f5c59/your-cron-logged-success-every-day-for-weeks-it-approved-nothing-1llg</guid>
      <description>&lt;p&gt;Your cron logged success every day for weeks. It approved nothing.&lt;/p&gt;




&lt;p&gt;We run ARIA, an autonomous content pipeline at Elevare Digital. Agents generate drafts, a daily approver cron reviews them, approved content moves to publishing. Fully automated, gated by a human-review layer, self-healing in most places.&lt;/p&gt;

&lt;p&gt;For several weeks, the approver cron ran on schedule, logged &lt;code&gt;{ status: 'success' }&lt;/code&gt;, and approved zero drafts. Nineteen drafts sat in the queue. Nobody was alerted. The system looked healthy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A cron that processes zero rows and a cron that processes zero rows &lt;em&gt;because there's nothing to process&lt;/em&gt; produce identical logs.&lt;/li&gt;
&lt;li&gt;PostgREST inner joins silently exclude rows when the join condition matches nothing — no error, no warning, just an empty result set.&lt;/li&gt;
&lt;li&gt;Producer/consumer type drift is invisible until you instrument the gap between "rows waiting" and "rows processed."&lt;/li&gt;
&lt;li&gt;Alert on &lt;code&gt;pending &amp;gt; 0 AND processed === 0&lt;/code&gt;, not just on thrown errors.&lt;/li&gt;
&lt;li&gt;Autonomous systems fail most dangerously in the space between errors and correctness.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What the system was doing
&lt;/h2&gt;

&lt;p&gt;ARIA's generator agents write content and store drafts as rows in a &lt;code&gt;content_opportunities&lt;/code&gt; table. Each row has an &lt;code&gt;opportunity_type&lt;/code&gt; — in practice, the generator was producing &lt;code&gt;'article'&lt;/code&gt; and &lt;code&gt;'listing'&lt;/code&gt; types.&lt;/p&gt;

&lt;p&gt;The approver cron queries that table via Supabase / PostgREST and processes any pending drafts it finds. Here's the shape of what it was doing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// approver-cron/index.ts (Deno edge function)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;opportunities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content_opportunities&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
    id,
    title,
    status,
    content_threads!inner(
      id,
      body
    )
  `&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending_review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;opportunity_type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;thread&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- the problem&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Approver query failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Approver run complete. Processed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opportunities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ... process opportunities&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things combined to create the silent failure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;opportunity_type = 'thread'&lt;/code&gt; — the generator never wrote rows with this type. It wrote &lt;code&gt;'article'&lt;/code&gt; and &lt;code&gt;'listing'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;content_threads!inner(...)&lt;/code&gt; — PostgREST inner join syntax. If no matching rows exist in &lt;code&gt;content_threads&lt;/code&gt;, the parent rows are excluded entirely.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: &lt;code&gt;opportunities&lt;/code&gt; was always an empty array. No error was thrown. &lt;code&gt;opportunities.length&lt;/code&gt; logged as &lt;code&gt;0&lt;/code&gt;. The function returned &lt;code&gt;200 OK&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the worst kind of failure
&lt;/h2&gt;

&lt;p&gt;Most failures announce themselves. A network timeout throws. A missing env var crashes on startup. A malformed query returns a PostgREST error object.&lt;/p&gt;

&lt;p&gt;This failure looked like rest.&lt;/p&gt;

&lt;p&gt;In a queue system, there's a legitimate state where the consumer runs and finds nothing to do — because the queue is empty. That's healthy-idle. It logs &lt;code&gt;Processed: 0&lt;/code&gt; and exits cleanly.&lt;/p&gt;

&lt;p&gt;Our broken state also logged &lt;code&gt;Processed: 0&lt;/code&gt; and exited cleanly.&lt;/p&gt;

&lt;p&gt;From the outside, from a log aggregator, from a dashboard: identical.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Healthy idle — queue is empty
Approver run complete. Processed: 0

// Broken — 19 drafts waiting, filter excludes all of them  
Approver run complete. Processed: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The only way to tell them apart is to check whether the queue &lt;em&gt;actually had pending rows&lt;/em&gt; when the consumer ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: broaden scope, instrument the gap
&lt;/h2&gt;

&lt;p&gt;The immediate fix was to remove the incorrect type filter and fix the join. The approver should consume all pending drafts, not just ones of a type the generator never produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fixed: consume what the producer actually writes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;opportunities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content_opportunities&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
    id,
    title,
    status,
    opportunity_type
  `&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending_review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// No type filter. No inner join restricting to non-existent rows.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the more important fix was the alert. After each consumer run, we now check whether there are pending rows that weren't touched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After the approver processes its batch:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;opportunities&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;stillPending&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content_opportunities&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending_review&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stillPending&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Consumer ran, touched nothing, but the queue has rows.&lt;/span&gt;
  &lt;span class="c1"&gt;// This is the danger zone.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;triggerAlert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consumer_drift&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Approver processed 0 rows but queue has &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stillPending&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; pending drafts.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Processed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Still pending: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stillPending&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This check doesn't care &lt;em&gt;why&lt;/em&gt; the consumer touched zero rows. It cares that the queue had work and the consumer didn't do it. The reason might be a bad filter, a broken join, a type mismatch, or something else we haven't thought of yet. The alert fires regardless and forces a human to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  The underlying pattern: producer/consumer drift
&lt;/h2&gt;

&lt;p&gt;The generator and approver were written at different times. At some point, the generator's output types and the approver's filter drifted apart. Neither system knew about the other's contract. There was no shared schema validation between them, no test that asserted "the approver can actually see what the generator writes."&lt;/p&gt;

&lt;p&gt;This is a normal way systems decay. The producer evolves. The consumer doesn't. Or vice versa. In a synchronous API call, this kind of mismatch usually causes a visible failure. In a queue-based async pipeline, it just causes the queue to grow while the consumer reports success.&lt;/p&gt;

&lt;p&gt;Autonomous systems make this worse because there's no human in the loop watching the queue depth during normal operation. The whole point is that it runs without supervision. Which means the instrumentation &lt;em&gt;is&lt;/em&gt; the supervision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'd do differently
&lt;/h2&gt;

&lt;p&gt;A few things that would have caught this earlier:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log scanned vs. processed separately.&lt;/strong&gt; If your consumer query returns zero rows, log that distinctly from "query returned rows and we processed them." &lt;code&gt;scanned: 0, processed: 0&lt;/code&gt; vs &lt;code&gt;scanned: 5, processed: 5&lt;/code&gt; are different states.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track queue depth over time.&lt;/strong&gt; If pending rows are accumulating across multiple cron runs, that's a signal even without a consumer error. A simple check: if the queue depth at the end of a run is higher than at the start, something isn't working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test the consumer against real producer output.&lt;/strong&gt; Not just "does the function run without throwing" but "given a row the producer actually writes, does the consumer find and process it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name your contracts.&lt;/strong&gt; If the producer writes &lt;code&gt;opportunity_type: 'article'&lt;/code&gt; and the consumer filters on &lt;code&gt;opportunity_type: 'thread'&lt;/code&gt;, that's a broken contract. Treat it like one — define it explicitly, validate it, test it.&lt;/p&gt;




&lt;p&gt;The honest summary: a filter we forgot about, combined with an inner join that excluded everything, turned our approver cron into a machine that ran on schedule, logged success, and accomplished nothing for weeks. The fix was straightforward. The lesson is that in autonomous pipelines, success-on-empty is a failure mode you have to design against explicitly, because the logs will never tell you on their own.&lt;/p&gt;

&lt;p&gt;Alert on the gap between what the queue holds and what the consumer touches. Not just on errors.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;— Mike Clarke, founder of Elevare Digital.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>coding</category>
      <category>webdev</category>
      <category>learning</category>
    </item>
    <item>
      <title>Smart Traffic Systems: Understanding and Configuring the Core Logic</title>
      <dc:creator>Mike Clarke</dc:creator>
      <pubDate>Wed, 01 Jul 2026 06:00:19 +0000</pubDate>
      <link>https://dev.to/mike_clarke_50a95013f5c59/smart-traffic-systems-understanding-and-configuring-the-core-logic-8hp</link>
      <guid>https://dev.to/mike_clarke_50a95013f5c59/smart-traffic-systems-understanding-and-configuring-the-core-logic-8hp</guid>
      <description>&lt;h3&gt;
  
  
  Stuck in Traffic? Let's Build a Smarter Way Forward.
&lt;/h3&gt;

&lt;p&gt;We've all been there: staring at a red light, no cross-traffic in sight, and wondering &lt;em&gt;why&lt;/em&gt; it's taking so long. Traditional, fixed-time traffic light systems are notoriously inefficient. They don't adapt to real-world conditions. But what if we could make them smarter? What if our traffic infrastructure could &lt;em&gt;think&lt;/em&gt;? &lt;/p&gt;

&lt;p&gt;That's where smart traffic systems come in. As developers, we have the power to build these adaptive, intelligent solutions that promise smoother commutes, reduced congestion, and lower emissions. But how do you &lt;em&gt;configure&lt;/em&gt; one?&lt;/p&gt;

&lt;h3&gt;
  
  
  The Brains Behind the Green Light: What Drives a Smart Traffic System?
&lt;/h3&gt;

&lt;p&gt;At its heart, a smart traffic system is a dynamic control loop. It observes, analyzes, decides, and then acts. Here's a breakdown of the key components you'll be configuring:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data Ingestion:&lt;/strong&gt; This is your system's eyes and ears. We're talking about real-time input from diverse sources: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Vehicle Sensors:&lt;/strong&gt; Inductive loops, radar, lidar, cameras – detecting vehicle presence, speed, and count.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Pedestrian Sensors:&lt;/strong&gt; Infrared, pressure plates, computer vision for safe pedestrian crossings.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Environmental Data:&lt;/strong&gt; Time of day, weather conditions, historical traffic patterns.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Traffic Flow Analysis &amp;amp; Prediction:&lt;/strong&gt; Once you have the data, you need to make sense of it. This involves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Queue Length Estimation:&lt;/strong&gt; How many cars are waiting at each intersection approach?&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Travel Time Prediction:&lt;/strong&gt; How long will it take to clear the intersection?&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Pattern Recognition:&lt;/strong&gt; Identifying recurring congestion points or anomalies.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Decision-Making Engine (The Optimization Algorithm):&lt;/strong&gt; This is the core intelligence. Based on the analyzed data, the algorithm determines the optimal light phasing and timing. Common approaches include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Reinforcement Learning:&lt;/strong&gt; The system learns optimal strategies through trial and error over time.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Fuzzy Logic:&lt;/strong&gt; Handling imprecise or uncertain data (e.g., 'heavy traffic').&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Heuristic Algorithms:&lt;/strong&gt; Rule-based systems designed to achieve specific goals (e.g., minimize wait time, maximize throughput).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Actuation:&lt;/strong&gt; Finally, the system sends commands to the traffic light controllers to change their state (red, yellow, green) and duration.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A Glimpse Under the Hood: Pseudocode for a Basic Adaptive System
&lt;/h3&gt;

&lt;p&gt;Let's consider a simplified scenario: a single intersection with four approaches. We want to minimize average wait time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FUNCTION ConfigureSmartTrafficSystem(intersectionID):
    // 1. Initialize Sensors &amp;amp; Data Streams
    sensors = GetSensorDataStreams(intersectionID)
    historical_data = LoadHistoricalTrafficPatterns(intersectionID)

    // 2. Define Optimization Goals (e.g., minimize average wait time, maximize throughput)
    optimization_goal = MINIMIZE_AVG_WAIT_TIME
    constraints = { MIN_GREEN_TIME: 10_seconds, MAX_GREEN_TIME: 60_seconds, YELLOW_TIME: 3_seconds }

    // 3. Main Control Loop
    LOOP indefinitely:
        current_vehicle_counts = ReadSensorData(sensors)
        current_pedestrian_counts = ReadPedestrianSensorData(sensors)
        current_time_of_day = GetCurrentTime()

        // 4. Analyze Traffic State
        queue_lengths = CalculateQueueLengths(current_vehicle_counts)
        demand_per_approach = EstimateDemand(current_vehicle_counts, historical_data, current_time_of_day)

        // 5. Decision-Making (using a simplified heuristic)
        next_phase_durations = DetermineOptimalPhaseDurations(
            queue_lengths,
            demand_per_approach,
            optimization_goal,
            constraints
        )

        // Example heuristic: Prioritize approach with most vehicles, but ensure fairness
        FUNCTION DetermineOptimalPhaseDurations(queues, demand, goal, constraints):
            priorities = CalculatePriorities(queues, demand) // e.g., higher queue means higher priority
            selected_phase = SelectPhaseBasedOnPriorities(priorities) 

            // Calculate green time for selected phase
            green_time = CalculateDynamicGreenTime(queues[selected_phase], constraints.MIN_GREEN_TIME, constraints.MAX_GREEN_TIME)

            RETURN { selected_phase: green_time, other_phases: constraints.YELLOW_TIME }

        // 6. Actuate Traffic Lights
        ApplyTrafficLightPhasing(intersectionID, next_phase_durations)

        WAIT(sum(next_phase_durations)) // Wait for the current cycle to complete
END FUNCTION
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configuring this system involves fine-tuning your sensor interpretation, refining your prediction models, and, critically, selecting and optimizing your decision-making algorithm. Are you prioritizing throughput, fairness, emergency vehicle preemption, or perhaps a blend? Each choice impacts the system's behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Practice Matters: Beyond the Pseudocode
&lt;/h3&gt;

&lt;p&gt;Understanding the concepts is one thing; making them work in a complex, dynamic environment is another. Real-world traffic data is noisy, sensors can fail, and unexpected events (accidents, sudden surges in traffic) will constantly challenge your system. You'll need to consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Robustness:&lt;/strong&gt; How does your system handle missing or erroneous data?&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Scalability:&lt;/strong&gt; Can it manage hundreds or thousands of intersections?&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Latency:&lt;/strong&gt; Can it react quickly enough to changing conditions?&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Ethical Considerations:&lt;/strong&gt; Are your algorithms fair across different districts or demographic areas?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't just theory; it's about building resilient, impactful software. The best way to grasp these complexities is by doing. Simulating different scenarios, tweaking algorithms, and observing their outcomes is crucial.&lt;/p&gt;

&lt;p&gt;Practice this concept interactively on CodeCityApp — free trial at codecityapp.com&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://codecityapp.com" rel="noopener noreferrer"&gt;CodeCityApp&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>coding</category>
      <category>programming</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>CodeCityApp vs LeetCode: Which is better for beginners in 2026?</title>
      <dc:creator>Mike Clarke</dc:creator>
      <pubDate>Wed, 24 Jun 2026 06:00:04 +0000</pubDate>
      <link>https://dev.to/mike_clarke_50a95013f5c59/codecityapp-vs-leetcode-which-is-better-for-beginners-in-2026-30nb</link>
      <guid>https://dev.to/mike_clarke_50a95013f5c59/codecityapp-vs-leetcode-which-is-better-for-beginners-in-2026-30nb</guid>
      <description>&lt;p&gt;undefined&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://codecityapp.com" rel="noopener noreferrer"&gt;CodeCityApp&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>coding</category>
      <category>programming</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to configure the smart traffic system</title>
      <dc:creator>Mike Clarke</dc:creator>
      <pubDate>Wed, 24 Jun 2026 06:00:04 +0000</pubDate>
      <link>https://dev.to/mike_clarke_50a95013f5c59/how-to-configure-the-smart-traffic-system-3a6c</link>
      <guid>https://dev.to/mike_clarke_50a95013f5c59/how-to-configure-the-smart-traffic-system-3a6c</guid>
      <description>&lt;p&gt;undefined&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://codecityapp.com" rel="noopener noreferrer"&gt;CodeCityApp&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>coding</category>
      <category>programming</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Technical Interview Hasn't Changed. That's Not an Oversight.</title>
      <dc:creator>Mike Clarke</dc:creator>
      <pubDate>Thu, 18 Jun 2026 21:23:45 +0000</pubDate>
      <link>https://dev.to/mike_clarke_50a95013f5c59/the-technical-interview-hasnt-changed-thats-not-an-oversight-1bi6</link>
      <guid>https://dev.to/mike_clarke_50a95013f5c59/the-technical-interview-hasnt-changed-thats-not-an-oversight-1bi6</guid>
      <description>&lt;p&gt;&lt;em&gt;Mike Clarke is the founder of CodeCityApp (codecityapp.com), a programming education platform built on the premise that computer science fundamentals become more valuable as AI tooling becomes more accessible, not less. He writes on developer education, hiring, and the economics of technical skill.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Every six months, someone publishes a confident piece arguing that AI is about to kill the technical interview. The whiteboard is dead. LeetCode is theater. Why test what someone can build when you can just watch them build it?&lt;/p&gt;

&lt;p&gt;It's a good argument. It keeps not coming true.&lt;/p&gt;

&lt;p&gt;The technical interview — data structures, algorithms, system design, debugging under pressure — has survived AI code generation, the proliferation of AI-assisted IDEs, and two years of serious industry debate about whether it still signals anything real. The companies paying the highest salaries for remote developers still run it. They've refined it, but they haven't dropped it.&lt;/p&gt;

&lt;p&gt;The question is worth taking seriously: why not?&lt;/p&gt;

&lt;h2&gt;
  
  
  The answer isn't traditionalism
&lt;/h2&gt;

&lt;p&gt;It would be easy to dismiss technical interviews as institutional inertia — hiring managers who learned to interview a certain way and never updated their priors. Some of that is real. But that's not the full explanation for why the strongest technical employers kept the screen after AI made everything else negotiable.&lt;/p&gt;

&lt;p&gt;The interview tests something AI cannot do for you in the interview room: real-time reasoning about your own code.&lt;/p&gt;

&lt;p&gt;When a senior engineer at a well-run company asks you to walk through why your solution is O(n log n) rather than O(n²), or to trace what happens when your hash map hits a collision, or to redesign your architecture to handle 100x traffic, they're not testing whether you know the answer in advance. They're testing whether you understand what you built — whether the mental model is yours or borrowed.&lt;/p&gt;

&lt;p&gt;AI can generate a correct solution. It cannot, in the moment, explain that solution's behavior to a stranger who is probing its edge cases. The human in the chair has to do that. And that requires the fundamentals.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the proliferation of AI output has actually done
&lt;/h2&gt;

&lt;p&gt;The counterintuitive effect of AI code generation on technical hiring is that it raised the stakes for fundamentals, not lowered them.&lt;/p&gt;

&lt;p&gt;When a junior developer could only produce what they personally knew how to write, their portfolio was an imperfect but real signal of skill. A functioning CRUD app in 2019 probably meant the person understood HTTP, databases, and some state management.&lt;/p&gt;

&lt;p&gt;A functioning CRUD app in 2025 means less. Any developer who can construct a reasonable prompt can ship one. The surface-level output has been democratized to the point of being uninformative.&lt;/p&gt;

&lt;p&gt;So hiring managers probed deeper. The interview became more important as the portfolio became less informative. The whiteboard survived because it tests the one thing that AI output doesn't prove: whether you can think through the code, not just produce it.&lt;/p&gt;

&lt;p&gt;CodeCityApp, a programming education platform that teaches CS fundamentals through game-based mechanics rather than rote drill, has tracked this shift directly with the developers it trains. The platforms and bootcamps that pivoted to AI-tool-first curricula saw learner output improve and interview pass rates decline. The correlation is consistent enough across five regions — West Africa, South America, India, Southeast Asia, Eastern Europe — to be worth stating plainly: AI tooling improves what you can build and does not improve whether you can explain it.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is good news for the right developers
&lt;/h2&gt;

&lt;p&gt;Framing this as "AI is making things harder" misses the opportunity.&lt;/p&gt;

&lt;p&gt;If AI production is commoditized and fundamental understanding is scarce, the economic value of understanding has increased. The developer who genuinely knows what their code is doing is more valuable today than they were three years ago, not less, because the supply of developers who can produce output without that understanding has grown dramatically.&lt;/p&gt;

&lt;p&gt;The markets that have built genuine technical talent — not resume-stuffing AI output but actual computer science depth — are the ones that will keep producing developers that clear international remote hiring filters. That's Nigeria's Andela cohorts. That's Brazil's competitive CS universities. That's India's IIT alumni. That's Eastern Europe's historically strong mathematics-based programming traditions.&lt;/p&gt;

&lt;p&gt;The technical interview hasn't changed because the job hasn't changed. You still have to debug code that doesn't work in a production system that your employer depends on, without an AI that knows your full codebase context and has the right answer. The screen tests for that. It'll keep testing for that until the job changes.&lt;/p&gt;

&lt;p&gt;The job hasn't changed.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;CodeCityApp (codecityapp.com) is a programming education platform that uses city-building game mechanics to teach computer science fundamentals. The platform serves learners in West Africa, South America, South and Southeast Asia, Eastern Europe, and the UK. Research data cited in this article draws on learner outcome tracking from CodeCityApp institutional partners across these regions.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
      <category>career</category>
    </item>
    <item>
      <title>Unlocking Efficiency: How to Configure a Smart Traffic System</title>
      <dc:creator>Mike Clarke</dc:creator>
      <pubDate>Wed, 17 Jun 2026 06:00:13 +0000</pubDate>
      <link>https://dev.to/mike_clarke_50a95013f5c59/unlocking-efficiency-how-to-configure-a-smart-traffic-system-1ndf</link>
      <guid>https://dev.to/mike_clarke_50a95013f5c59/unlocking-efficiency-how-to-configure-a-smart-traffic-system-1ndf</guid>
      <description>&lt;p&gt;Hey folks, let's talk traffic. Not the kind that makes you want to pull your hair out on your morning commute, but the kind we, as developers, can actually &lt;em&gt;solve&lt;/em&gt;. We're talking about &lt;strong&gt;how to configure a smart traffic system&lt;/strong&gt; – a complex beast, but one that offers immense satisfaction when you get it right.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: Legacy Systems and Gridlock
&lt;/h3&gt;

&lt;p&gt;Traditional traffic light systems are, frankly, dumb. They operate on fixed timers, blissfully unaware of the actual traffic flow. This leads to bottlenecks, unnecessary idling, increased emissions, and frustrated drivers. As populations grow and urban sprawl continues, this problem only intensifies. Enter the smart traffic system – a dynamic, data-driven approach to keeping things moving.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Concept: Data-Driven Traffic Flow
&lt;/h3&gt;

&lt;p&gt;At its core, a smart traffic system isn't about magic; it's about intelligent data processing and adaptive control. Imagine a network of sensors (inductive loops, cameras, lidar) constantly feeding real-time data about vehicle presence, speed, and density at every intersection. This data is then crunched by a central control unit or distributed edge devices, which then dynamically adjust traffic light timings, lane assignments, and even integrate with public transport or emergency services.&lt;/p&gt;

&lt;p&gt;Key components often include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Sensors:&lt;/strong&gt; Gathering raw traffic data.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Data Aggregation &amp;amp; Preprocessing:&lt;/strong&gt; Cleaning and structuring the raw data.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Decision Engine/AI:&lt;/strong&gt; The brains of the operation, using algorithms (think reinforcement learning, fuzzy logic, or predictive models) to determine optimal light timings.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Actuators:&lt;/strong&gt; The traffic lights themselves, variable message signs, and other controlled elements.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Communication Network:&lt;/strong&gt; Ensuring low-latency data transfer between all components.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Centralized/Distributed Control:&lt;/strong&gt; Depending on the scale and architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuring the Brain: Pseudocode Example
&lt;/h3&gt;

&lt;p&gt;Let's sketch out a greatly simplified pseudocode example for dynamic light timing at a single intersection. Real-world systems are far more complex, but this illustrates the core idea.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FUNCTION ConfigureTrafficSystem(IntersectionID, SensorData)

  // 1. Collect Real-time Data
  TrafficCounts_North = GetSensorData(IntersectionID, 'NorthBound')
  TrafficCounts_East = GetSensorData(IntersectionID, 'EastBound')
  // ... and so on for other directions

  // 2. Assess Current Congestion
  CongestionLevel_North = CalculateCongestion(TrafficCounts_North)
  CongestionLevel_East = CalculateCongestion(TrafficCounts_East)
  // ...

  // 3. Apply Decision Logic (Simplified Example: Prioritize Heaviest Flow)
  IF CongestionLevel_North &amp;gt; CongestionLevel_East AND CongestionLevel_North &amp;gt; Threshold
    SetLightTiming(IntersectionID, 'NorthSouth', 'Green', DynamicDuration_High)
    SetLightTiming(IntersectionID, 'EastWest', 'Red', DynamicDuration_Low)
  ELSE IF CongestionLevel_East &amp;gt; CongestionLevel_North AND CongestionLevel_East &amp;gt; Threshold
    SetLightTiming(IntersectionID, 'EastWest', 'Green', DynamicDuration_High)
    SetLightTiming(IntersectionID, 'NorthSouth', 'Red', DynamicDuration_Low)
  ELSE // Default or balanced state
    SetLightTiming(IntersectionID, 'NorthSouth', 'Green', DefaultDuration)
    SetLightTiming(IntersectionID, 'EastWest', 'Red', DefaultDuration)
  END IF

  // 4. Incorporate Special Conditions (e.g., Emergency Vehicles, Public Transport Priority)
  IF EmergencyVehicleDetected(IntersectionID, 'NorthBound')
    ForceGreen(IntersectionID, 'NorthSouth')
  END IF

  // 5. Log and Monitor for Optimization
  LogSystemState(IntersectionID, CurrentTimings, CongestionLevels)
  // Trigger re-evaluation periodically or on significant data changes

END FUNCTION
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pseudocode barely scratches the surface. A production-ready system would involve sophisticated predictive algorithms, machine learning models trained on historical data, inter-intersection communication to prevent cascading blockages, and robust error handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Practice Matters: Beyond the Whiteboard
&lt;/h3&gt;

&lt;p&gt;Conceptualizing a smart traffic system is one thing; actually building and configuring it is another. The interplay of sensors, algorithms, communication protocols, and physical actuators introduces a myriad of challenges. How do you handle sensor failures? What's the optimal data sampling rate? How do you ensure low latency in control signals? These are questions best answered through hands-on practice, simulation, and experimentation.&lt;/p&gt;

&lt;p&gt;Understanding the architectural patterns, the nuances of real-time data processing, and the impact of different algorithmic choices is crucial. You'll need to think about system scalability, fault tolerance, and security – often in a distributed environment. It's a fantastic area to flex your problem-solving muscles and apply a broad range of development skills.&lt;/p&gt;

&lt;p&gt;Practice this concept interactively on CodeCityApp — free trial at codecityapp.com&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://codecityapp.com" rel="noopener noreferrer"&gt;CodeCityApp&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>coding</category>
      <category>programming</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why I Built a City-Building Coding Platform Instead of Another Tutorial Site</title>
      <dc:creator>Mike Clarke</dc:creator>
      <pubDate>Sun, 14 Jun 2026 20:52:26 +0000</pubDate>
      <link>https://dev.to/mike_clarke_50a95013f5c59/why-i-built-a-city-building-coding-platform-instead-of-another-tutorial-site-29nm</link>
      <guid>https://dev.to/mike_clarke_50a95013f5c59/why-i-built-a-city-building-coding-platform-instead-of-another-tutorial-site-29nm</guid>
      <description>&lt;h1&gt;
  
  
  Why I Built a City-Building Coding Platform Instead of Another Tutorial Site
&lt;/h1&gt;

&lt;p&gt;When I started CodeCity three years ago, I wasn't trying to disrupt anything. I was trying to solve a problem I couldn't ignore.&lt;/p&gt;

&lt;p&gt;I'd spent 25 years as an enterprise architect. I've interviewed thousands of developers, hired hundreds, and mentored plenty more. And I noticed something that bothered me: the gap between "learning to code" and "actually coding consistently" was impossibly wide.&lt;/p&gt;

&lt;p&gt;You could complete a tutorial. You could ace a LeetCode problem. But the second you closed the browser tab, the momentum evaporated. I watched smart people stop coding after weeks because there was nothing pulling them back.&lt;/p&gt;

&lt;p&gt;The irony was that they &lt;em&gt;wanted&lt;/em&gt; to keep going. They just didn't have a reason that stuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tutorial Trap
&lt;/h2&gt;

&lt;p&gt;The market already had plenty of tutorial platforms. I could've built another Codecademy clone—interactive lessons, progressive difficulty, completion badges. It would've been the sensible thing to do.&lt;/p&gt;

&lt;p&gt;But here's what I know from two decades in tech: tutorials teach you &lt;em&gt;about&lt;/em&gt; programming. They don't teach you programming.&lt;/p&gt;

&lt;p&gt;There's a real cognitive difference. Tutorials hold your hand through carefully scaffolded problems. You follow the steps. You see the output. Dopamine. Repeat.&lt;/p&gt;

&lt;p&gt;But when you close the tutorial and face a blank file, something breaks. The training wheels are gone. You're alone with your problem, your editor, and your panic.&lt;/p&gt;

&lt;p&gt;I kept thinking about the developers I'd mentored who actually &lt;em&gt;stuck&lt;/em&gt; with coding. What did they have in common? They all had a reason to show up. A project. A goal. Something beyond "I completed Module 7."&lt;/p&gt;

&lt;p&gt;They weren't optimizing for tutorial completion. They were building something.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Creates Consistency
&lt;/h2&gt;

&lt;p&gt;Around this time, I got back into gaming—mostly narrative-driven stuff like Baldur's Gate 3. And I noticed something interesting: I'd play a game for 20 minutes intending to spend the evening coding. Then an hour would pass. Then two.&lt;/p&gt;

&lt;p&gt;I wasn't a gamer. I don't have "addictive personality" tattooed on my forehead. But something about the game structure—the progression loop, the visible growth, the just-one-more-thing feeling—made me come back without thinking about it.&lt;/p&gt;

&lt;p&gt;That's when it clicked: what if I could separate the &lt;em&gt;progression mechanic&lt;/em&gt; from the &lt;em&gt;game narrative&lt;/em&gt; and apply it to something that actually mattered? What if instead of leveling up a character, you were leveling up &lt;em&gt;yourself&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;The city metaphor came naturally. Every problem you solve builds something tangible. The buildings don't disappear. They're &lt;em&gt;yours&lt;/em&gt;. Your skyline is your record. And unlike a badge on a platform you might forget about, your city is something you see every time you log in.&lt;/p&gt;

&lt;p&gt;It's a small thing, but it matters. When you've built 47 buildings, you don't want to stop at 48. When your city looks sparse compared to someone else's, you want to add more.&lt;/p&gt;

&lt;p&gt;That's not manipulation. It's just honest architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mechanics Behind Stickiness
&lt;/h2&gt;

&lt;p&gt;I needed to be careful here. Gamification has a bad reputation because most implementations are lazy—add points and badges, call it done. But real gamification isn't about fake rewards. It's about making the &lt;em&gt;real work&lt;/em&gt; feel like progress.&lt;/p&gt;

&lt;p&gt;For CodeCity, that meant:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visible progression that can't be faked.&lt;/strong&gt; Every building represents a real coding challenge you solved. You can't buy buildings. You can't get them as gifts. They're earned. This matters psychologically. Your city is authentically &lt;em&gt;yours&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variable difficulty, not linear punishment.&lt;/strong&gt; I wasn't going to build a platform where skipping harder problems made your city look sad. Instead, I wanted challenges across 17 languages, from Python to Rust, where everyone could find problems at their level. Some people are grinding Python; some are leveling up in Go. The city scales with you, not against you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Competition without toxicity.&lt;/strong&gt; Arena battles exist, but they're optional. Some people love 1v1 matches; some never touch them. Both paths grow your city. I've seen LeetCode destroy people's confidence by making them feel permanently ranked. Here, you're competing if you want to, but the real competition is with yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI mentor angle.&lt;/strong&gt; I trained ARIA to give hints, not answers. Not because I'm noble, but because I know from experience: if you get the answer, you get the dopamine hit and move on. If you get a hint and work through it, you get &lt;em&gt;two&lt;/em&gt; dopamine hits—the hint, then the solve—and you remember it longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reality Check
&lt;/h2&gt;

&lt;p&gt;Three years in, here's what actually happens: people check in on CodeCity between meetings. They solve problems on lunch breaks. They build for months, not days.&lt;/p&gt;

&lt;p&gt;Are they optimizing for some perfect learning experience? No. They're just... coding regularly. Building habits. That's the actual goal.&lt;/p&gt;

&lt;p&gt;Do I have all the answers? Absolutely not. We're still figuring out how to keep people in the 6-month range from abandoning platforms (that's a brutal cliff). We're learning which challenge types stick and which feel like busywork.&lt;/p&gt;

&lt;p&gt;But I know we're doing something different because I hear from solo founders, boot camp grads, and career-switchers saying the same thing: "I actually use this. I actually come back."&lt;/p&gt;

&lt;p&gt;That's not revolutionary language. It's just accurate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Building CodeCity taught me that consistency beats intelligence in programming. The person who codes 30 minutes every day will outpace the person who crams on weekends, even if the latter is smarter.&lt;/p&gt;

&lt;p&gt;And tools should amplify that. Not by being tricky or manipulative, but by removing friction and making the &lt;em&gt;real work&lt;/em&gt; feel like something you want to do again tomorrow.&lt;/p&gt;

&lt;p&gt;That's why I didn't build another tutorial site. That's why I built a city.&lt;/p&gt;




&lt;p&gt;If you've hit the wall where tutorials aren't sticking, or you're tired of LeetCode grind culture, come build a city at &lt;a href="https://codecityapp.com" rel="noopener noreferrer"&gt;CodeCity&lt;/a&gt;. Free tier, no limits that matter, and 1,300+ problems across every language worth learning.&lt;/p&gt;

&lt;p&gt;What actually keeps &lt;em&gt;you&lt;/em&gt; coding consistently? I'd love to hear it.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>learning</category>
      <category>coding</category>
      <category>gamification</category>
    </item>
    <item>
      <title>Navigating the Digital Arteries: How to Configure a Smart Traffic System</title>
      <dc:creator>Mike Clarke</dc:creator>
      <pubDate>Wed, 10 Jun 2026 06:00:15 +0000</pubDate>
      <link>https://dev.to/mike_clarke_50a95013f5c59/navigating-the-digital-arteries-how-to-configure-a-smart-traffic-system-12op</link>
      <guid>https://dev.to/mike_clarke_50a95013f5c59/navigating-the-digital-arteries-how-to-configure-a-smart-traffic-system-12op</guid>
      <description>&lt;h2&gt;
  
  
  Navigating the Digital Arteries: How to Configure a Smart Traffic System
&lt;/h2&gt;

&lt;p&gt;Ever found yourself stuck in interminable traffic, wondering if there’s a better way? As developers, we instinctively translate real-world problems into solvable algorithms. Traffic congestion is a prime candidate. Traditional traffic light systems are often static, failing to adapt to real-time conditions. This is where &lt;strong&gt;smart traffic systems&lt;/strong&gt; come in – dynamic, data-driven solutions designed to optimize flow, reduce wait times, and even cut down on emissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: Static vs. Dynamic
&lt;/h3&gt;

&lt;p&gt;Imagine a city where traffic lights operate on fixed timings, regardless of whether it's rush hour or midnight, or if there's an emergency vehicle needing passage. It's inefficient, frustrating, and a waste of resources. The core problem is the lack of real-time responsiveness. Our goal, when configuring a smart traffic system, is to inject that responsiveness, turning dumb intersections into intelligent hubs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Concept: Data-Driven Decision Making
&lt;/h3&gt;

&lt;p&gt;At its heart, a smart traffic system relies on a continuous feedback loop. Sensors (inductive loops, cameras, radar, even GPS data from vehicles) collect real-time data on vehicle presence, density, speed, and even wait times at each approach. This data is fed into a central processing unit (often leveraging edge computing or cloud platforms) which then uses algorithms to predict future flow and make instantaneous decisions about light sequencing and timing.&lt;/p&gt;

&lt;p&gt;Key components you’ll be dealing with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Data Ingestion Layer&lt;/strong&gt;: How raw sensor data is collected and formatted.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Processing &amp;amp; Analytics Engine&lt;/strong&gt;: Where the magic happens – algorithms, machine learning models, and rule-based systems.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Actuation Layer&lt;/strong&gt;: Sending commands back to the traffic light controllers.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Communication Protocols&lt;/strong&gt;: The lifeblood enabling data exchange (e.g., MQTT, Kafka, gRPC).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The configuration challenge lies in defining the rules, training the models, and ensuring seamless communication between these layers. You're not just flipping a switch; you're building a brain for the intersection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pseudocode: The Brain of an Intersection
&lt;/h3&gt;

&lt;p&gt;Let’s outline a simplified &lt;em&gt;SmartTrafficController&lt;/em&gt; routine. Imagine this running for each intersection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLASS SmartTrafficController:
    METHOD initialize(intersection_id, default_cycle_time):
        SELF.id = intersection_id
        SELF.approaches = GET_ALL_APPROACHES(intersection_id) // North, South, East, West
        SELF.current_phase_index = 0
        SELF.phase_durations = [DEFAULT_GREEN_TIME, DEFAULT_YELLOW_TIME, DEFAULT_RED_TIME] // Example
        SELF.sensor_data = {} // Store real-time data for each approach
        SELF.traffic_prediction_model = LOAD_PREDICTIVE_MODEL()

    METHOD update_sensor_data(new_data):
        FOR approach_id, data IN new_data:
            SELF.sensor_data[approach_id] = data // e.g., vehicle_count, avg_speed, queue_length

    METHOD calculate_optimal_phase_duration():
        predicted_traffic_inflow = SELF.traffic_prediction_model.predict(SELF.sensor_data, HISTORICAL_DATA)
        emergency_vehicle_nearby = CHECK_EMERGENCY_SERVICES_DATABASE(SELF.id)

        IF emergency_vehicle_nearby:
            RETURN { 'North': 0, 'East': 0, 'South': MAX_GREEN_TIME, 'West': 0 } // Prioritize
        ELSE IF predicted_traffic_inflow['dominant_approach'] &amp;gt; THRESHOLD:
            // Implement a dynamic algorithm (e.g., actuated, adaptive, fuzzy logic)
            // Adjust green times based on current and predicted demand
            optimal_green_times = ADAPTIVE_OPTIMIZATION_ALGORITHM(SELF.sensor_data, predicted_traffic_inflow)
            RETURN optimal_green_times
        ELSE:
            RETURN SELF.phase_durations // Fallback to default or observed patterns

    METHOD apply_phase_changes():
        optimal_durations = SELF.calculate_optimal_phase_duration()
        // Cycle through approaches, setting lights based on optimal_durations
        FOREACH approach IN SELF.approaches:
            current_approach_id = SELF.approaches[SELF.current_phase_index]
            SET_LIGHTS(current_approach_id, GREEN, optimal_durations[current_approach_id]['green'])
            WAIT(optimal_durations[current_approach_id]['green'])
            SET_LIGHTS(current_approach_id, YELLOW, optimal_durations[current_approach_id]['yellow'])
            WAIT(optimal_durations[current_approach_id]['yellow'])
            SET_LIGHTS(current_approach_id, RED, optimal_durations[current_approach_id]['red'])
            WAIT(optimal_durations[current_approach_id]['red'])

        SELF.current_phase_index = (SELF.current_phase_index + 1) % LENGTH(SELF.approaches)

// Main Loop for the city's traffic system
WHILE TRUE:
    FOR EACH intersection IN ALL_INTERSECTIONS:
        LATEST_SENSOR_DATA = GET_ALL_SENSOR_DATA(intersection.id)
        intersection.update_sensor_data(LATEST_SENSOR_DATA)
        intersection.apply_phase_changes()
    SLEEP(DECISION_INTERVAL)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pseudocode shows the structural flow: data comes in, decisions are made using models and rules, and then actions are taken. Real-world systems are, of course, far more complex, involving distributed systems, robust error handling, and sophisticated algorithms like Reinforcement Learning. But this is the core loop you'll be building around.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Practice Matters
&lt;/h3&gt;

&lt;p&gt;Understanding the concepts is one thing; actually implementing and troubleshooting is another. When you’re dealing with real-time data streams, concurrency, and potentially life-critical infrastructure, theoretical understanding simply isn't enough. You need to get your hands dirty with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;State Management&lt;/strong&gt;: How do you keep track of the current state of &lt;em&gt;thousands&lt;/em&gt; of traffic lights?&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Concurrency&lt;/strong&gt;: How do multiple intersections make decisions simultaneously without conflicts?&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Scalability&lt;/strong&gt;: How does your system handle an increasing number of intersections and data points?&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Algorithm Tuning&lt;/strong&gt;: How do you find the sweet spot for your traffic optimization algorithms?&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Edge Cases&lt;/strong&gt;: What happens during sensor failure? Power outages? Massive unpredictable events?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Take Control of the Digital Intersection
&lt;/h3&gt;

&lt;p&gt;Configuring a smart traffic system touches on IoT, big data, machine learning, and distributed systems. It's a fantastic problem domain for honing your development skills. Don't just read about it; build it.&lt;/p&gt;

&lt;p&gt;Practice this concept interactively on CodeCityApp — free trial at codecityapp.com&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://codecityapp.com" rel="noopener noreferrer"&gt;CodeCityApp&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>coding</category>
      <category>programming</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I vibe-coded 1,334 coding challenges. Here's the ugly part nobody talks about.</title>
      <dc:creator>Mike Clarke</dc:creator>
      <pubDate>Mon, 08 Jun 2026 23:00:49 +0000</pubDate>
      <link>https://dev.to/mike_clarke_50a95013f5c59/i-vibe-coded-1334-coding-challenges-heres-the-ugly-part-nobody-talks-about-37f0</link>
      <guid>https://dev.to/mike_clarke_50a95013f5c59/i-vibe-coded-1334-coding-challenges-heres-the-ugly-part-nobody-talks-about-37f0</guid>
      <description>&lt;p&gt;I used Lovable, Supabase, and Claude to build a 1,334-challenge coding platform in a few months. Most posts about this kind of build are highlight reels. This one isn't. Here's what actually broke, what the AI confidently got wrong, and the three things I had to rebuild in raw code because no tool would touch them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The RLS disaster I didn't see coming
&lt;/h2&gt;

&lt;p&gt;Lovable generates Supabase tables fast. What it doesn't do is think about Row Level Security.&lt;/p&gt;

&lt;p&gt;I had user progress data leaking across accounts for two weeks before I caught it. Every user could query every other user's challenge completions. The AI had created the tables, the policies even &lt;em&gt;looked&lt;/em&gt; right at a glance — but &lt;code&gt;using (true)&lt;/code&gt; was the condition on every policy. Wide open.&lt;/p&gt;

&lt;p&gt;I stopped building entirely, audited every table, and rewrote all the RLS by hand. Three days. The AI kept regenerating the same broken pattern that caused the problem in the first place.&lt;/p&gt;

&lt;p&gt;Lesson: never let a vibe coding tool near your security layer. Write RLS by hand or don't ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The curriculum structure I rebuilt twice
&lt;/h2&gt;

&lt;p&gt;1,334 challenges sounds like a lot. The problem is organizing them so users progress logically and completion tracking doesn't fall apart under load.&lt;/p&gt;

&lt;p&gt;My first schema had challenges flat in one table. Simple. Wrong.&lt;/p&gt;

&lt;p&gt;Around 200 challenges the ordering logic became a mess and queries slowed down. I rebuilt with a node/edge structure — challenges as nodes, dependencies as edges. That's not something Lovable generates naturally. I wrote the migration SQL myself.&lt;/p&gt;

&lt;p&gt;Four days gone. Lovable kept suggesting more columns instead of a better structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edge functions the AI refuses to get right
&lt;/h2&gt;

&lt;p&gt;Every time I asked Lovable or Claude to write a Supabase edge function, it produced code that worked in isolation and failed in production. Wrong CORS headers, incorrect Deno import patterns, env variable access that broke on deploy.&lt;/p&gt;

&lt;p&gt;I have twelve edge functions running in production. All twelve were rewritten by hand. The AI gave me a useful starting point maybe 40% of the time. The rest it actively misled me — confidently, quickly, and wrongly.&lt;/p&gt;

&lt;p&gt;My rule now: write edge functions in raw Deno, test locally with &lt;code&gt;supabase functions serve&lt;/code&gt;, and never trust a generated function until it has passed three real deploys.&lt;/p&gt;

&lt;h2&gt;
  
  
  What vibe coding actually is after doing it at scale
&lt;/h2&gt;

&lt;p&gt;It's not "describe what you want and get a working app." It's closer to: AI writes the 70% that's predictable, you own the 30% that requires real understanding of your stack.&lt;/p&gt;

&lt;p&gt;The 30% is always the same three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security (RLS, auth logic, service role access)&lt;/li&gt;
&lt;li&gt;Data structure decisions that affect everything downstream&lt;/li&gt;
&lt;li&gt;Anything crossing a boundary — frontend → edge function → external API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building something small and throwaway, vibe coding is great. If you're shipping to real users with real data, plan to own those three layers yourself from day one.&lt;/p&gt;




&lt;p&gt;What broke in your last AI build? Drop it below — curious whether people are hitting the same walls.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>vibecoding</category>
      <category>ai</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
