<?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: Aaron Ross</title>
    <description>The latest articles on DEV Community by Aaron Ross (@ashmortar).</description>
    <link>https://dev.to/ashmortar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3744797%2F873541b7-0cbf-4767-ac88-40eed88d5c14.png</url>
      <title>DEV Community: Aaron Ross</title>
      <link>https://dev.to/ashmortar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ashmortar"/>
    <language>en</language>
    <item>
      <title>Free DevOps Notifications with Discord and GitHub Actions</title>
      <dc:creator>Aaron Ross</dc:creator>
      <pubDate>Tue, 10 Feb 2026 04:26:25 +0000</pubDate>
      <link>https://dev.to/ashmortar/free-devops-notifications-with-discord-and-github-actions-10b9</link>
      <guid>https://dev.to/ashmortar/free-devops-notifications-with-discord-and-github-actions-10b9</guid>
      <description>&lt;p&gt;I run a few open source projects, and I don't have a team. It's just me, which means there's nobody else to notice when the 6am cron job fails. I'm certainly not going to sit there refreshing the GitHub Actions page to find out.&lt;/p&gt;

&lt;p&gt;Slack would work for this, but paying for Slack so that one person can send notifications to themselves feels a little sad when you think about it. Discord servers are free, GitHub Actions are free for public repos, and Discord webhooks are just HTTP endpoints that accept JSON. You probably see where this is going.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pitch
&lt;/h2&gt;

&lt;p&gt;I now get push notifications on my phone when my scheduled jobs fail, and I didn't pay anyone for the privilege. The whole setup took an evening, and most of that was figuring out how I wanted to format the messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a Webhook URL
&lt;/h2&gt;

&lt;p&gt;Right-click a channel in Discord, go to Edit Channel, then Integrations, then Webhooks. Create one, copy the URL, and add it as a secret in your GitHub repo (I called mine &lt;code&gt;DISCORD_WEBHOOK_URL&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;That's genuinely it. No OAuth dance, no bot tokens, no registering an application with Discord's developer portal. Just a URL that accepts POST requests with JSON bodies. If you've ever set up a Slack app you're probably crying right now, and honestly, it's okay. Let it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Script
&lt;/h2&gt;

&lt;p&gt;After a few iterations of inline bash in my workflow files (which worked but was getting repetitive), I pulled everything into a reusable script. Here's what it looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;TITLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nv"&gt;COLOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nv"&gt;DESCRIPTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nv"&gt;BUTTONS&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$# &lt;/span&gt;&lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  case&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="nt"&gt;--title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;TITLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift &lt;/span&gt;2 &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="nt"&gt;--color&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;COLOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift &lt;/span&gt;2 &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="nt"&gt;--description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;DESCRIPTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift &lt;/span&gt;2 &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="nt"&gt;--button&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; BUTTONS+&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift &lt;/span&gt;2 &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Unknown arg: &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="k"&gt;done

if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DISCORD_WEBHOOK&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"DISCORD_WEBHOOK not set, skipping notification"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +&lt;span class="s2"&gt;"%Y-%m-%dT%H:%M:%SZ"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DISCORD_WEBHOOK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Convert buttons to Discord component format&lt;/span&gt;
&lt;span class="nv"&gt;BUTTONS_JSON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"[]"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;BUTTONS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;BUTTONS_JSON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUTTONS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
    jq &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s1"&gt;'split("|") | {type: 2, style: 5, label: .[0], url: (.[1:] | join("|"))}'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
    jq &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?with_components=true"&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;PAYLOAD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--arg&lt;/span&gt; title &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TITLE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--argjson&lt;/span&gt; color &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COLOR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--arg&lt;/span&gt; description &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DESCRIPTION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--arg&lt;/span&gt; timestamp &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TIMESTAMP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--argjson&lt;/span&gt; buttons &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUTTONS_JSON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{
    username: "DD Notification Bot",
    avatar_url: "https://democracy-direct.com/logo-square.png",
    embeds: [{
      title: $title,
      color: $color,
      description: $description,
      footer: { text: "Democracy Direct CI" },
      timestamp: $timestamp
    }]
  } + if ($buttons | length) &amp;gt; 0 then {
    components: [{ type: 1, components: $buttons }]
  } else {} end'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PAYLOAD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$WEBHOOK_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save that to &lt;code&gt;.github/scripts/discord-notify.sh&lt;/code&gt;, make it executable, and now you can call it from any workflow with a simple interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;.github/scripts/discord-notify.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"🚨 Something Broke"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--color&lt;/span&gt; 15158332 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"The thing that was supposed to work did not work."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--button&lt;/span&gt; &lt;span class="s2"&gt;"View Run|https://github.com/..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--button&lt;/span&gt; &lt;span class="s2"&gt;"Production|https://your-site.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--button&lt;/code&gt; format is just &lt;code&gt;"Label|URL"&lt;/code&gt;. You can have multiple buttons and they'll show up as actual clickable Discord buttons, not just markdown links. The script also gracefully skips sending if the webhook secret isn't set, which is nice for forks or local testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Use It For
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://democracy-direct.com" rel="noopener noreferrer"&gt;Democracy Direct&lt;/a&gt; has scheduled jobs that sync data from the Congress API every morning (legislators, votes, bills, that sort of thing). For these, "it passed" isn't enough information. I want to know whether anything actually changed, how much changed, and if something failed, which specific part failed.&lt;/p&gt;

&lt;p&gt;The notifications look like this when things go well:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk8a2ikmdtr9t2b6p7ssw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk8a2ikmdtr9t2b6p7ssw.png" alt="Discord notification showing data legislation backfill results" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And like this when they don't:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkvnuixqbzr7x16ghxu13.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkvnuixqbzr7x16ghxu13.png" alt="Discord notification showing failed smoke tests" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The workflow builds up a description string based on what each sync step reported, picks a color based on the overall status, and calls the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Send Discord notification&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DISCORD_WEBHOOK&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DISCORD_WEBHOOK_URL }}&lt;/span&gt;
    &lt;span class="na"&gt;LEGISLATORS_RESULT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.legislators.outputs.result }}&lt;/span&gt;
    &lt;span class="na"&gt;LEGISLATORS_FAILED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.legislators.outputs.failed }}&lt;/span&gt;
    &lt;span class="c1"&gt;# ... etc for other steps&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;# Parse the JSON output from each step&lt;/span&gt;
    &lt;span class="s"&gt;LEG_CHANGED=$(echo "$LEGISLATORS_RESULT" | jq -r '.changed // false')&lt;/span&gt;
    &lt;span class="s"&gt;LEG_COUNT=$(echo "$LEGISLATORS_RESULT" | jq -r '.recordsUpserted // 0')&lt;/span&gt;

    &lt;span class="s"&gt;# Determine overall status and pick a color&lt;/span&gt;
    &lt;span class="s"&gt;if [ "$LEGISLATORS_FAILED" = "true" ] || [ "$VOTES_FAILED" = "true" ]; then&lt;/span&gt;
      &lt;span class="s"&gt;TITLE="🚨 Data Refresh Failed"&lt;/span&gt;
      &lt;span class="s"&gt;COLOR=15158332  # red&lt;/span&gt;
    &lt;span class="s"&gt;elif [ "$LEG_CHANGED" = "true" ] || [ "$VOTES_CHANGED" = "true" ]; then&lt;/span&gt;
      &lt;span class="s"&gt;TITLE="📊 Data Refresh Complete"&lt;/span&gt;
      &lt;span class="s"&gt;COLOR=3066993  # green&lt;/span&gt;
    &lt;span class="s"&gt;else&lt;/span&gt;
      &lt;span class="s"&gt;TITLE="✅ Data Refresh - No Changes"&lt;/span&gt;
      &lt;span class="s"&gt;COLOR=9807270  # gray&lt;/span&gt;
    &lt;span class="s"&gt;fi&lt;/span&gt;

    &lt;span class="s"&gt;# Build the description&lt;/span&gt;
    &lt;span class="s"&gt;if [ "$LEGISLATORS_FAILED" = "true" ]; then&lt;/span&gt;
      &lt;span class="s"&gt;LEG_STATUS="❌ **Legislators**: Failed"&lt;/span&gt;
    &lt;span class="s"&gt;elif [ "$LEG_CHANGED" = "true" ]; then&lt;/span&gt;
      &lt;span class="s"&gt;LEG_STATUS="✅ **Legislators**: ${LEG_COUNT} upserted"&lt;/span&gt;
    &lt;span class="s"&gt;else&lt;/span&gt;
      &lt;span class="s"&gt;LEG_STATUS="⏭️ **Legislators**: Unchanged"&lt;/span&gt;
    &lt;span class="s"&gt;fi&lt;/span&gt;

    &lt;span class="s"&gt;# ... similar for votes, bills, etc&lt;/span&gt;

    &lt;span class="s"&gt;DESC="${LEG_STATUS}"$'\n'"${VOTES_STATUS}"$'\n'"${BILLS_STATUS}"&lt;/span&gt;
    &lt;span class="s"&gt;GH_RUN="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"&lt;/span&gt;

    &lt;span class="s"&gt;.github/scripts/discord-notify.sh \&lt;/span&gt;
      &lt;span class="s"&gt;--title "$TITLE" \&lt;/span&gt;
      &lt;span class="s"&gt;--color "$COLOR" \&lt;/span&gt;
      &lt;span class="s"&gt;--description "$DESC" \&lt;/span&gt;
      &lt;span class="s"&gt;--button "View Run|$GH_RUN"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is having your sync scripts output structured JSON (&lt;code&gt;{"changed": true, "recordsUpserted": 42}&lt;/code&gt;). Once you have that, you can format it however you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Oh No Production Is Broken" Notification
&lt;/h2&gt;

&lt;p&gt;I also run smoke tests after Cloudflare finishes deploying to production. These only send a notification on failure, because I don't need a pat on the head every time something works correctly. I'm not a golden retriever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Send Discord notification on failure&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;failure()&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;DISCORD_WEBHOOK&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DISCORD_WEBHOOK_URL }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;GH_RUN="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"&lt;/span&gt;

    &lt;span class="s"&gt;.github/scripts/discord-notify.sh \&lt;/span&gt;
      &lt;span class="s"&gt;--title "🚨 Smoke Test Failed" \&lt;/span&gt;
      &lt;span class="s"&gt;--color 15158332 \&lt;/span&gt;
      &lt;span class="s"&gt;--description "One or more smoke tests failed against production." \&lt;/span&gt;
      &lt;span class="s"&gt;--button "View Run|$GH_RUN" \&lt;/span&gt;
      &lt;span class="s"&gt;--button "Production|https://democracy-direct.com" \&lt;/span&gt;
      &lt;span class="s"&gt;--button "Cloudflare|https://dash.cloudflare.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three buttons: one to the GitHub Action run, one to production so I can see if it's actually broken, and one to Cloudflare in case I need to roll back. Everything I need to start debugging, right there in the notification.&lt;/p&gt;

&lt;h2&gt;
  
  
  "But There Are GitHub Actions For This Already"
&lt;/h2&gt;

&lt;p&gt;There are, and they're fine for simple cases. I didn't use them because:&lt;/p&gt;

&lt;p&gt;First, there's no official Discord action. These are all third-party, which means you're trusting some random developer's JavaScript (or Docker container) to run in your CI pipeline with access to your secrets. The most popular one even recommends pinning to a specific commit SHA for "stability purposes," which is really security advice dressed up in polite language. I'd rather just use curl.&lt;/p&gt;

&lt;p&gt;Second, it's literally just curl and jq, both of which are already installed on GitHub runners. No dependencies to install, no Docker images to pull, no supply chain to worry about.&lt;/p&gt;

&lt;p&gt;Third, I can actually read it. The whole notification logic is right there in my repo where I can see it. Want to change something? Just change it. Want to copy it to another project? Copy and paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Discord Instead of Slack
&lt;/h2&gt;

&lt;p&gt;If your company is already paying for Slack and you have SSO requirements and compliance needs and all 2,600 of those integrations, then by all means, use Slack.&lt;/p&gt;

&lt;p&gt;But if you're a solo dev working on open source projects, or a small team that doesn't want to pay per-seat pricing for a chat app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discord is actually free, not "free but we delete your message history after 90 days" free&lt;/li&gt;
&lt;li&gt;No per-seat pricing, which matters less when you're one person but matters a lot if your project grows and you want to bring on contributors&lt;/li&gt;
&lt;li&gt;Mobile notifications work great, which is really all I wanted in the first place&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff is that Discord doesn't have deep integrations with Jira or Salesforce or whatever enterprise tools you might use at a day job. For a project that lives entirely in GitHub and deploys to Cloudflare, I genuinely do not care about that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go Look at the Actual Files
&lt;/h2&gt;

&lt;p&gt;The complete workflow files and the notification script are in the &lt;a href="https://github.com/anomalousventures/democracy-direct" rel="noopener noreferrer"&gt;Democracy Direct repo&lt;/a&gt; if you want to see how everything fits together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/anomalousventures/democracy-direct/blob/main/.github/scripts/discord-notify.sh" rel="noopener noreferrer"&gt;discord-notify.sh&lt;/a&gt; - The reusable notification script&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anomalousventures/democracy-direct/blob/main/.github/workflows/refresh-data.yml" rel="noopener noreferrer"&gt;refresh-data.yml&lt;/a&gt; - Daily sync with detailed status reporting&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anomalousventures/democracy-direct/blob/main/.github/workflows/smoke-test.yml" rel="noopener noreferrer"&gt;smoke-test.yml&lt;/a&gt; - Post-deploy verification that only alerts on failure&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anomalousventures/democracy-direct/blob/main/.github/workflows/sync-sponsored-bills.yml" rel="noopener noreferrer"&gt;sync-sponsored-bills.yml&lt;/a&gt; - Weekly job with summary notifications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're not pristine examples of software engineering. The jq pipelines are kind of dense if you're not familiar with the syntax. But they work, and you're welcome to steal whatever parts are useful to you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://democracy-direct.com" rel="noopener noreferrer"&gt;Democracy Direct&lt;/a&gt; is an open source civic engagement platform. &lt;a href="https://github.com/anomalousventures/democracy-direct" rel="noopener noreferrer"&gt;Check it out on GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>github</category>
      <category>discord</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The Eyes Have It: Closing the Agentic Design Loop</title>
      <dc:creator>Aaron Ross</dc:creator>
      <pubDate>Tue, 03 Feb 2026 23:05:56 +0000</pubDate>
      <link>https://dev.to/ashmortar/the-eyes-have-it-closing-the-agentic-design-loop-3819</link>
      <guid>https://dev.to/ashmortar/the-eyes-have-it-closing-the-agentic-design-loop-3819</guid>
      <description>&lt;p&gt;The thing that makes LLMs actually work for coding, aside from reviewing the output carefully, is tight feedback loops.&lt;/p&gt;

&lt;p&gt;Something that can be unit tested is easy to get an LLM to complete. You describe the acceptance criteria, have it write the tests, then follow TDD until the task is done. Review, look for vulnerabilities and edge cases, repeat as necessary. You can even take the human out of parts of this loop with automated code review on PRs. Let the bots fight it out until they're satisfied, then bring in the human to verify.&lt;/p&gt;

&lt;p&gt;I've been a developer for almost 10 years. I've worked across stacks and languages. I've had clients patent my work. I'm not saying this to brag, I'm saying it because when I tell you that front-end design has been the single most frustrating part of using language models for coding, I want you to know it's not because I don't know what I'm doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UI Feedback Loop is Broken
&lt;/h2&gt;

&lt;p&gt;When it comes to user experience, the feedback loop has historically been really frustrating. It goes something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You: "Center the modal and add some padding"&lt;/li&gt;
&lt;li&gt;AI: &lt;em&gt;changes code&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;You: "It's centered but now there's way too much padding"&lt;/li&gt;
&lt;li&gt;AI: "How much padding would you like?"&lt;/li&gt;
&lt;li&gt;You: "Less. Like, half that. Also the close button is too close to the edge now"&lt;/li&gt;
&lt;li&gt;AI: &lt;em&gt;changes code&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;You: "The padding is better but you broke the close button positioning"&lt;/li&gt;
&lt;li&gt;AI: "Can you describe what's wrong with the close button?"&lt;/li&gt;
&lt;li&gt;You: &lt;em&gt;gives up, opens the CSS file, fixes it in 30 seconds&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I usually don't make it past step 5. It ends up being way faster for me to just make the styling edits manually because I know CSS and the robot often doesn't do things in a modern or holistic way.&lt;/p&gt;

&lt;p&gt;(I've seen more &lt;code&gt;!important&lt;/code&gt; flags from Claude and Cursor than from all of the junior devs I've worked with combined. Reward hacking is a real problem with LLMs.)&lt;/p&gt;

&lt;p&gt;You can paste screenshots into the chat, and that helps. But then you're doing it manually every time, and you have to remember to do it, and you're still the one who has to notice something looks off before you think to take a screenshot. It's better than pure description, but it's still a lot of back and forth where you, the human, are the bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the Loop
&lt;/h2&gt;

&lt;p&gt;Claude Code has a &lt;code&gt;--chrome&lt;/code&gt; flag that connects it to a browser extension. This gives it tools to take screenshots, resize the viewport, click around, and generally interact with your browser. The workflow changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You: "Make the contact form button more prominent"&lt;/li&gt;
&lt;li&gt;AI: &lt;em&gt;changes code, takes screenshot, notices the double border it just introduced&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;AI: "Done. I also noticed the border change created a double-border issue where the preview container meets the button area, so I fixed that too."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Or even better: you notice something wrong and say "the preview tab has a double border." Instead of guessing at why, the agent can go to the page, click the preview button, and take a screenshot. Context is king, and &lt;em&gt;what it looks like&lt;/em&gt; is the most valuable UI context there is.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Set This Up
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Install the &lt;a href="https://chromewebstore.google.com/publisher/anthropic/u308d63ea0533efcf7ba778ad42da7390" rel="noopener noreferrer"&gt;Claude in Chrome extension&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Log into claude.ai in Chrome&lt;/li&gt;
&lt;li&gt;Start Claude Code with &lt;code&gt;claude --chrome&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The browser tools become available automatically. This gives Claude Code the ability to take screenshots, click and scroll around the page, resize the viewport to test responsive breakpoints, and record interactions as GIFs (useful for docs or sharing progress).&lt;/p&gt;

&lt;p&gt;A few things worth knowing: you need a dev server running since Claude can't refresh the browser for you. Auth-gated pages work fine as long as you're already logged in when you start the session. Hot reload works great with this setup (that's kind of the whole point).&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Example: Redesigning Democracy Direct's Contact Flow
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://democracy-direct.com" rel="noopener noreferrer"&gt;Democracy Direct&lt;/a&gt; is a civic engagement tool I've been building. You find your representatives and write them letters. The main interface is the ContactFlow component, and it needed work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The old layout:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4q7rft0smkqx8gt0u6f3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4q7rft0smkqx8gt0u6f3.png" alt="old sprawling design" width="800" height="2207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything stacked vertically: user info fields at top, editor, preview, "Send Your Letter" button, then a whole separate "Print &amp;amp; Mail" section with address fields and formatting checkboxes. Problems were everywhere. Editor and Preview stacked vertically meant lots of scrolling. Actions buried at the bottom. Print options always visible even though most people use the digital flow. On mobile, you'd scroll past the entire letter to find "Send."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The new layout:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonc1jumvqoukxw94ullw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fonc1jumvqoukxw94ullw.png" alt="new compact design" width="800" height="1193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two-column layout with the sidebar on the right. View mode buttons (Edit/Preview/Print Preview) replace the stacked editor and preview. Actions are always visible at the bottom (disabled when empty, not hidden). Print formatting options only appear in Print Preview mode. "Send via Contact Form" is now the hero action, impossible to miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On mobile:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzfjyjyc8j1i993k4pmnb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzfjyjyc8j1i993k4pmnb.png" alt="new compact design on mobile" width="380" height="3395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The sidebar ("Your Information") appears above the editor via CSS &lt;code&gt;order&lt;/code&gt; utilities. Users fill in their info before scrolling to the letter. Action buttons stack into a 2-column grid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Visual Feedback Actually Helped
&lt;/h2&gt;

&lt;p&gt;A few specific moments from the session where having screenshots made a real difference:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mobile reordering.&lt;/strong&gt; The sidebar needed to appear above the editor on mobile (so users fill in their info before scrolling to the letter) but beside it on desktop. I could have described this verbally and hoped Claude understood, but it was faster to just have it resize the viewport to 375px wide and verify the CSS &lt;code&gt;order&lt;/code&gt; utilities worked correctly. Screenshot, confirm, move on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The double border.&lt;/strong&gt; When we wrapped the &lt;code&gt;&amp;lt;LetterPreview /&amp;gt;&lt;/code&gt; in a container with its own border, we got a double border where it met the existing preview border. Claude caught this on the screenshot immediately ("I notice there's a double border issue") and fixed it in the same response. This is exactly the kind of thing I'd notice ten minutes later while testing something else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Button layout on mobile.&lt;/strong&gt; Getting the action buttons to form a sensible 2-column grid on mobile while staying in a row on desktop required iteration. Being able to resize the viewport and screenshot after each change made this converge quickly instead of becoming a back-and-forth about what "the buttons look weird on mobile" meant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;This isn't revolutionary technology. It's just closing a feedback loop that was previously open. LLMs work well when they can verify their own output. For most code, that means tests. For UI, that means screenshots.&lt;/p&gt;

&lt;p&gt;The result is fewer rounds of "what do you mean by weird spacing" and more rounds of actual iteration. For a civic engagement tool where the UI directly impacts whether someone successfully contacts their representative, that matters.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Democracy Direct is an open source project focused on making civic engagement more accessible. &lt;a href="https://github.com/anomalousventures/democracy-direct" rel="noopener noreferrer"&gt;Check it out on GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>ux</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built a civic engagement platform because contacting your representatives shouldn't be this hard</title>
      <dc:creator>Aaron Ross</dc:creator>
      <pubDate>Sun, 01 Feb 2026 06:21:48 +0000</pubDate>
      <link>https://dev.to/ashmortar/i-built-a-civic-engagement-platform-because-contacting-your-representatives-shouldnt-be-this-hard-1jjb</link>
      <guid>https://dev.to/ashmortar/i-built-a-civic-engagement-platform-because-contacting-your-representatives-shouldnt-be-this-hard-1jjb</guid>
      <description>&lt;p&gt;A few weeks ago I was doomscrolling and feeling some type of way about... &lt;em&gt;waves indiscriminately at the state of the world&lt;/em&gt;. The thing that always gets me during times like these is feeling like there's nothing I can do about any of it.&lt;/p&gt;

&lt;p&gt;So I had a good think about what I actually &lt;em&gt;can&lt;/em&gt; do. And I landed on something that's been bugging me for years: contacting your elected representatives is unreasonably hard. You have to dig through multiple government websites to figure out who represents you, find contact info in outdated directories, and then stare at a blank text box wondering what to even say. It's enough friction that most people just... don't.&lt;/p&gt;

&lt;p&gt;I'm a software engineer with almost 10 years of web development experience. I can't fix policy but I can fix a bad user experience. So I built &lt;a href="https://democracy-direct.com" rel="noopener noreferrer"&gt;Democracy Direct&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does right now
&lt;/h2&gt;

&lt;p&gt;You put in your ZIP code and see your two senators and your representative with their contact info and social media links. You can save your district so you don't have to look it up every time.&lt;/p&gt;

&lt;p&gt;The feature I'm most excited about is letter templates. Somebody writes a good letter about healthcare or housing or whatever, and other people can grab it, customize it, and send it. One person's effort becomes a tool for a lot of people. Templates can be public or private, and they include short descriptions so they show up well in search results and social sharing. No account needed to browse or use templates. Your messages never touch the server, everything stays on your device.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's coming
&lt;/h2&gt;

&lt;p&gt;Next up is voting records and legislation tracking. I want you to be able to see how your reps actually voted, search bills, read plain-language summaries, and link letter templates directly to specific legislation. After that, campaign finance data and an interactive district map. Longer term the goal is to expand beyond federal to cover state legislators and local officials too.&lt;/p&gt;

&lt;p&gt;The roadmap is public: &lt;a href="https://democracy-direct.com/roadmap" rel="noopener noreferrer"&gt;democracy-direct.com/roadmap&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy first
&lt;/h2&gt;

&lt;p&gt;This was non-negotiable from the start. ZIP code lookups happen entirely in your browser using pre-loaded data. We never see what you search. If you create an account, your email gets hashed with SHA-256 before it's stored. We can't recover it even if we wanted to. Letters you write never touch the server. Analytics are anonymous through PostHog with no session recordings and no way to link usage data back to you.&lt;/p&gt;

&lt;p&gt;Cloudflare Turnstile keeps bots from spamming the template system, and I'm using AI moderation to help with content review so one person can actually run this thing without drowning in moderation work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;I wanted to move fast, ship something that loads fast, and deploy to Cloudflare Pages. That meant Astro with React and TypeScript, which is also the sweet spot for working with AI coding agents. I've been building most of this with &lt;a href="https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; and experimenting with &lt;a href="https://github.com/anthropics/claude-code/tree/main/plugins/ralph-loop" rel="noopener noreferrer"&gt;Ralph loops&lt;/a&gt; throughout the project with varying degrees of success. When it works it's like having a junior dev who never sleeps. When it doesn't you're cleaning up some truly creative messes.&lt;/p&gt;

&lt;p&gt;The database is &lt;a href="https://neon.tech" rel="noopener noreferrer"&gt;Neon&lt;/a&gt; PostgreSQL. On-demand compute, easy branching and replication per PR, and a generous free tier. Authentication is OTP codes via AWS SES. No passwords to manage, no passwords to forget, no credential stuffing to worry about.&lt;/p&gt;

&lt;p&gt;Representative data comes from public sources: &lt;a href="https://github.com/unitedstates/congress-legislators" rel="noopener noreferrer"&gt;unitedstates/congress-legislators&lt;/a&gt; for federal, &lt;a href="https://openstates.org/" rel="noopener noreferrer"&gt;Open States&lt;/a&gt; for state legislators, and the U.S. Census Bureau for ZIP code mapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building in public
&lt;/h2&gt;

&lt;p&gt;The whole thing is open source under AGPL because civic infrastructure should be auditable and forkable. No ads, no sponsors, no premium tier, no data sales. Funded by &lt;a href="https://github.com/sponsors/ashmortar" rel="noopener noreferrer"&gt;donations&lt;/a&gt; if you're into that sort of thing.&lt;/p&gt;

&lt;p&gt;Democracy Direct is independent. No party, no PAC, no nonprofit. Just a frustrated engineer who decided to build something instead of doomscroll.&lt;/p&gt;

&lt;p&gt;Code's at &lt;a href="https://github.com/anomalousventures/democracy-direct" rel="noopener noreferrer"&gt;github.com/anomalousventures/democracy-direct&lt;/a&gt;. Contributions welcome, whether that's code, templates, bug reports, or whatever. If it's useful to you, share it with someone who'd use it. The whole point is making it easier for people to be heard.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>webdev</category>
      <category>ai</category>
      <category>react</category>
    </item>
  </channel>
</rss>
