<?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: webdev</title>
    <description>The latest articles tagged 'webdev' on DEV Community.</description>
    <link>https://dev.to/t/webdev</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tag/webdev"/>
    <language>en</language>
    <item>
      <title>Bàn giao website xong, 5 việc tuần đầu để trang thật sự dùng được</title>
      <dc:creator>Mạnh Digital</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:49:15 +0000</pubDate>
      <link>https://dev.to/manhdigital68/ban-giao-website-xong-5-viec-tuan-dau-de-trang-that-su-dung-duoc-col</link>
      <guid>https://dev.to/manhdigital68/ban-giao-website-xong-5-viec-tuan-dau-de-trang-that-su-dung-duoc-col</guid>
      <description>&lt;p&gt;Ngày bàn giao website thường được coi là ngày kết thúc dự án. Với chủ doanh nghiệp, đó mới là ngày trang bắt đầu phải làm việc. Một trang đẹp mà Google không biết tới, form gửi đi không ai nhận, tên miền hết hạn giữa chừng thì cũng như chưa có trang.&lt;/p&gt;

&lt;p&gt;Dưới đây là 5 việc nên làm trong tuần đầu sau bàn giao, kèm lệnh kiểm nhanh cho người làm web. Khách không biết code vẫn tự kiểm được phần lớn bằng trình duyệt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kiểm HTTPS và bản www chuyển về cùng một địa chỉ&lt;/strong&gt;&lt;br&gt;
Trình duyệt trên điện thoại không được báo cảnh báo bảo mật, và bản có www với bản không có www phải về cùng một địa chỉ.&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;# http phải chuyển hướng 301 sang https&lt;/span&gt;
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; http://example.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"^(HTTP|location)"&lt;/span&gt;
&lt;span class="c"&gt;# bản www phải chuyển về bản chính (hoặc ngược lại)&lt;/span&gt;
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://www.example.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"^(HTTP|location)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ghi ngày hết hạn tên miền và gói máy chủ vào lịch, đặt nhắc trước một tháng.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Khai báo Search Console, kiểm robots.txt và sitemap&lt;/strong&gt;&lt;br&gt;
Xác minh quyền sở hữu trong Google Search Console, gửi sitemap. Trước đó nên chắc robots.txt không chặn cả trang và sitemap có đủ URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/robots.txt
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/sitemap.xml | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;loc&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lỗi hay gặp nhất sau bàn giao là trang còn thẻ noindex từ lúc làm bản thử. Trên WordPress, đó thường là ô chặn công cụ tìm kiếm trong mục Cài đặt, Đọc.&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;# không được thấy "noindex" ở trang chủ và các trang dịch vụ&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-io&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;meta name="robots"[^&amp;gt;]*&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Đọc lại từng trang dịch vụ như một khách đang vội&lt;/strong&gt;&lt;br&gt;
Mỗi trang cần trả lời được 4 câu trong vài giây đầu: làm gì, cho ai, giá khoảng bao nhiêu, liên hệ bằng cách nào. Thẻ title nên chứa đúng tên dịch vụ khách hay gõ, không phải khẩu hiệu. Đây cũng là phần nền của SEO: Google và các trợ lý AI chỉ đưa trang ra khi đọc là hiểu trang đó bán gì.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thử gửi form và bấm gọi từ chính điện thoại của mình&lt;/strong&gt;&lt;br&gt;
Gửi một form thử, kiểm email hoặc tin nhắn có tới đúng người không. Bấm nút gọi và nút Zalo xem có mở đúng số không. Lỗi ở bước này tốn tiền nhất, vì khách đã quyết liên hệ mà không tới được.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gắn sự kiện đo trước khi chạy bất kỳ kênh nào&lt;/strong&gt;&lt;br&gt;
Cài Google Analytics 4 và bắn sự kiện khi khách thật sự hành động, không chỉ khi xem trang.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// form gửi thành công&lt;/span&gt;
&lt;span class="nf"&gt;gtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event&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;generate_lead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// bấm nút gọi&lt;/span&gt;
&lt;span class="nf"&gt;gtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event&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;click_call&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Đánh dấu generate_lead là sự kiện chính trong GA4 để vài tháng sau so được kênh nào mang khách.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nói trước với khách rằng trang không tự có khách&lt;/strong&gt;&lt;br&gt;
Phần lớn khách thất vọng với website không phải vì giao diện, mà vì trang không tự có người vào. Nói trước điều này và đưa danh sách trên cho khách, mối quan hệ sau bàn giao sẽ dễ hơn nhiều.&lt;/p&gt;

&lt;p&gt;Vì sao một doanh nghiệp nhỏ cần trang web, 6 việc nó làm được và 3 việc nó không làm thay được: &lt;a href="https://manhdigital.com/blog/trang-web-dung-de-lam-gi" rel="noopener noreferrer"&gt;trang web dùng để làm gì trên blog Mạnh Digital&lt;/a&gt;. Bản kiểm kỹ thuật dài hơn có trong bài &lt;a href="https://manhdigital.com/blog/website-chuan-seo" rel="noopener noreferrer"&gt;tiêu chí website chuẩn SEO&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  webdev #seo
&lt;/h1&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Reserved names: keeping your app's brand out of a user's handle</title>
      <dc:creator>Frank Milvus</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:48:08 +0000</pubDate>
      <link>https://dev.to/frankmilvus/reserved-names-keeping-your-apps-brand-out-of-a-users-handle-330i</link>
      <guid>https://dev.to/frankmilvus/reserved-names-keeping-your-apps-brand-out-of-a-users-handle-330i</guid>
      <description>&lt;p&gt;A name in a notification is a claim about who is talking. When the row says &lt;strong&gt;Folcadi Support&lt;/strong&gt;, most people read it as the app speaking, not as a stranger who picked a good title. That is the whole attack: cheap to set up, impossible to explain afterwards.&lt;/p&gt;

&lt;p&gt;This is the story of closing that gap in Folcadi, a language-exchange messenger. It covers the three holes that made impersonation possible, the one shared module that now closes them, and the design decision that followed: a reserved name &lt;em&gt;is&lt;/em&gt; the official channel, and an official channel behaves differently from a normal group.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to look official
&lt;/h2&gt;

&lt;p&gt;The app already had a reserved-name list for user handles. Group names had none. That asymmetry was the bug.&lt;/p&gt;

&lt;p&gt;Three separate holes, each enough on its own:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Group names were unprotected.&lt;/strong&gt; Anyone could create a group called "Folcadi" or "Folcadi Support". It would appear in other people's chat lists, and the invite push would read "Added to group: Folcadi". A group title travels further than a handle, because it shows up in someone else's notification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The handle rule lived only in the client.&lt;/strong&gt; The app limited nicknames to 2–20 characters of &lt;code&gt;[A-Za-z0-9_]&lt;/code&gt;. A direct request to the API was not bound by a form validator, so it could store anything the database accepted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matching was exact and untrimmed.&lt;/strong&gt; &lt;code&gt;"folcadi "&lt;/code&gt; with a trailing space renders identically to &lt;code&gt;folcadi&lt;/code&gt; but failed an exact-match comparison. The check was correct in the letter and useless in practice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Any of the three makes the other two look like details. Fixing one at a time would have left the same outcome reachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  One module, two call sites
&lt;/h2&gt;

&lt;p&gt;The fix starts with a single module that owns the rules: the reserved sets, the brand tokens, the normaliser and the nickname pattern. It has no database access and imports nothing from the feature layer, so both the profile path and the group path can use it without a cycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;RESERVED_NICKNAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;folcadi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;folcadi-admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;folcadi_official&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;folcad-i&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moderator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# retired brands stay reserved so nobody impersonates them either
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tongue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tongue-admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tongue_official&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gwing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gwing-admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gwing_official&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;RESERVED_GROUP_NAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;frozenset&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;folcadi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moderator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;BRAND_TOKENS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;folcadi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
&lt;span class="n"&gt;NICKNAME_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^[a-zA-Z0-9_]{2,20}$&lt;/span&gt;&lt;span class="sh"&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 important part is not the list. It is that there is exactly one list. Two copies of a security rule drift on schedule: someone adds a variant to one path during a review and forgets the other, and the hole reopens silently. A shared module turns "did we update both?" into a compiler question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalise before you compare
&lt;/h2&gt;

&lt;p&gt;Comparing user-supplied names is a normalisation problem first and a lookup problem second. The helper is deliberately boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Lowercase + collapse whitespace, closing the `&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;folcadi &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;` bypass.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lowercasing handles &lt;code&gt;Folcadi&lt;/code&gt;. Collapsing whitespace handles both &lt;code&gt;" folcadi"&lt;/code&gt; and &lt;code&gt;"folcadi "&lt;/code&gt;, plus the internal-tab variant that looks like a space. &lt;code&gt;.split()&lt;/code&gt; with no argument also drops the empty strings, so the join is the normalised form.&lt;/p&gt;

&lt;p&gt;Two matching strategies sit on top of it, and the difference is intentional:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact match&lt;/strong&gt; against &lt;code&gt;RESERVED_NICKNAMES&lt;/code&gt; and &lt;code&gt;RESERVED_GROUP_NAMES&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Substring match&lt;/strong&gt; against &lt;code&gt;BRAND_TOKENS&lt;/code&gt; — currently just the live brand name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The substring rule is the strict one: it also blocks "Folcadi fans" and "Folcadi Official". That is deliberate while there is no in-app badge that distinguishes a real official account from a convincing one. Over-blocking a few creative group names is cheaper than under-blocking one impersonation.&lt;/p&gt;

&lt;p&gt;Retired brands are exact-match only. This is easy to get wrong. A messenger for language learners needs to let someone name a group &lt;strong&gt;"Tongue twisters"&lt;/strong&gt;; only the retired brand &lt;em&gt;as a name&lt;/em&gt; is off-limits. The interesting rule lives in the comment next to the list, not in the code, because that is where the next person will change it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server is the boundary, not the form
&lt;/h2&gt;

&lt;p&gt;The nickname rule moved into a validation function used by every path that can write a nickname: the user's own profile edit, the admin edit, and the availability check behind the sign-up form.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_nickname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nickname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nickname&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_valid_nickname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NICKNAME_RULE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_reserved_nickname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Nickname already taken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two response details matter more than they look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invalid shape is a 400; reserved is "already taken".&lt;/strong&gt; The first tells the user to fix their input. The second should not confirm that &lt;code&gt;admin&lt;/code&gt; exists as a reserved slot — from the outside, "reserved" and "someone has it" are indistinguishable, and that is fine. Different status codes, one message for the outside world.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The stored value is the stripped value.&lt;/strong&gt; Returning the normalised result instead of validating a copy means the database only ever sees the shape that was checked. A validator that returns the original string is a subtle bug: it verifies &lt;code&gt;"  frank  "&lt;/code&gt; and then stores the spaces.&lt;/p&gt;

&lt;p&gt;Signup is the third writer, and it generates handles instead of asking for them. Generated names are truncated to fit the same pattern, so an account created through a provider never starts life with a handle it is not allowed to keep.&lt;/p&gt;

&lt;p&gt;Group names got the same treatment one layer earlier. The request schema trims the name &lt;strong&gt;before&lt;/strong&gt; the length validators run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@field_validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;before&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@classmethod&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_trim_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without that ordering, a whitespace-only name passes &lt;code&gt;min_length=1&lt;/code&gt;, and &lt;code&gt;" Folcadi "&lt;/code&gt; reaches the reserved check unnormalised. Validator order is part of the rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reserved name is the official group
&lt;/h2&gt;

&lt;p&gt;Once group names could not be taken by users, a product question appeared: what should &lt;em&gt;somebody&lt;/em&gt; use the name for?&lt;/p&gt;

&lt;p&gt;The app needed an official announcement channel — release notes, downtime notices, the things you want every account to see once. That needs a way to identify it. The tempting answer is a new column: &lt;code&gt;is_official&lt;/code&gt;, a migration, an admin toggle, and a permanently new concept in every query that reads a conversation.&lt;/p&gt;

&lt;p&gt;The cheaper answer was already in the schema. A reserved name is a marker nobody else can set. Staff can create or rename a group to a reserved name; everyone else gets a 403. So the name itself identifies the channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_official_group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Conversation&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;conv&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;is_reserved_group_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conv&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No column, no backfill, no second source of truth. The trade-off is honest: renaming the channel requires staff, and the behaviour is coupled to the name. But the invariant is enforced in the same module that protects the brand, so the coupling cannot drift either.&lt;/p&gt;

&lt;p&gt;Non-staff members cannot rename the group, transfer ownership, or dissolve it. Those are checked at the service layer, not just hidden in the interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  A channel you cannot silence
&lt;/h2&gt;

&lt;p&gt;For a normal group, the user's controls are the point. Mute it, block the owner, leave it. For an announcement channel, the same controls mean a user can quietly opt out of the one channel that carries "we are down for maintenance".&lt;/p&gt;

&lt;p&gt;So the official group changes three behaviours:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Muting is refused.&lt;/strong&gt; Setting a mute on a reserved-named group returns 403. Unmuting is still allowed, because a mute that predates the rule should be recoverable. Quiet hours still apply — that is a device-level setting about when you want to be disturbed, not a per-conversation decision, and respecting it is the difference between "cannot be silenced" and "wakes you at 3 a.m.".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blocking has no effect.&lt;/strong&gt; Group conversations already skipped block checks, and that stays true: blocking the owner of the channel does not stop announcements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Delete chat" only hides it.&lt;/strong&gt; This is the interesting one. Leaving a normal group deletes the membership row. Leaving the official group clears the member's reads, drafts, scheduled messages, private pins and unread rows, then sets a flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;is_official_group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conv&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# ...and the row stays in the database
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chat disappears from the list. The next message to the channel brings it back, because the broadcast path clears the flag before fan-out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ConversationMember&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ConversationMember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conversation_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ConversationMember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_hidden&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&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="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_hidden&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normal groups are untouched: leaving still deletes the membership. The asymmetry is the feature. "Delete chat" is a statement about the list you look at, not about the relationship you have with the team — and for the channel that carries service notices, the relationship has to survive it.&lt;/p&gt;

&lt;p&gt;One detail makes this work without new client code. The app already had a "message for an unknown group → reload the list" path. Setting the flag back to false server-side means the client's existing reaction re-renders the row. A server-side restore that needs no new client state is worth designing for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not solve
&lt;/h2&gt;

&lt;p&gt;Three limits are worth stating plainly, because a post about impersonation defence that claims completeness is the problem it describes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Homoglyphs are still possible.&lt;/strong&gt; Names are compared case-insensitively and whitespace-normalised, but not Unicode-normalised. A Cyrillic &lt;code&gt;а&lt;/code&gt; in &lt;code&gt;folcаdi&lt;/code&gt; looks identical in most fonts and is a different string. Real confusable detection is a separate project with its own data (mixed-script detection, skeleton mapping, a font-aware UI decision). Pretending the current normaliser covers it would be worse than saying it does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Substring blocking has a false-positive cost.&lt;/strong&gt; "Folcadi fans" is a natural group name and it is refused. That is the price of not having an official badge. When a badge exists, the rule can loosen. Until then, the product chooses the impersonation risk over the naming-convenience risk, and that is a judgment call, not a technical one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A reserved list is not a trust system.&lt;/strong&gt; It stops the cheap attack — pick the brand as your name and wait for someone to misread it. It does not stop a compromised device, a lookalike outside the app, or a user who believes a screenshot. Reserved names reduce the cost of being fooled for the laziest case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the tests pin
&lt;/h2&gt;

&lt;p&gt;Two suites cover the rules, and the interesting assertions are the negatives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"folcadi"&lt;/code&gt;, &lt;code&gt;"Folcadi"&lt;/code&gt;, &lt;code&gt;" Folcadi "&lt;/code&gt;, &lt;code&gt;"FOLCADI"&lt;/code&gt; are all rejected.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"folcadi "&lt;/code&gt; with a trailing space is rejected even though it is a different string than the one on the list.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"Tongue twisters"&lt;/code&gt; is accepted; &lt;code&gt;"tongue"&lt;/code&gt; is not.&lt;/li&gt;
&lt;li&gt;A non-staff member creating or renaming to a reserved group name gets 403.&lt;/li&gt;
&lt;li&gt;The official group refuses a mute, but accepts an unmute.&lt;/li&gt;
&lt;li&gt;Leaving the official group keeps the membership and sets the hidden flag; the next message clears it.&lt;/li&gt;
&lt;li&gt;Leaving a normal group deletes the membership, as before.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last pair is the one that catches regressions. A refactor that unifies the two leave paths would make all the positive tests pass and quietly break the official channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Strip away the app-specific parts and the shape is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One module owns every name rule&lt;/strong&gt; — reserved sets, normaliser, pattern, helpers. Two call sites, one source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalise before comparing&lt;/strong&gt; — case and whitespace at minimum, and say out loud what you are not normalising.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce on the server&lt;/strong&gt;, with the client as a convenience. A form validator is not a boundary; the API is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let the reserved name carry the identity&lt;/strong&gt; when it is already unique and staff-only. Fewer concepts, fewer migrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design the exception to the user's controls deliberately&lt;/strong&gt;, and document why the channel that carries service notices behaves differently from a chat with a friend.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of that is novel. The failure mode it prevents is boring too, which is the point: a name is a claim, and the app should decide which claims are available to anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why not just show a verified badge?
&lt;/h3&gt;

&lt;p&gt;A badge is the right long-term answer, and it needs a server-side source of truth first. Reserved names are that source of truth, and the badge renders it. Building the badge first would have meant a visual marker with nothing behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this change normal group names?
&lt;/h3&gt;

&lt;p&gt;Only when the name contains the live brand token or matches the reserved list. &lt;code&gt;"Language practice"&lt;/code&gt;, &lt;code&gt;"Book club"&lt;/code&gt;, and &lt;code&gt;"Tongue twisters"&lt;/code&gt; all pass. If a group name is refused, the reason is the 403 detail, not a silent rename.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens to a group that already had a reserved name?
&lt;/h3&gt;

&lt;p&gt;The rule is checked when a group is created or renamed. An existing group keeps its name until someone edits it; editing it then requires staff. The check is at the write boundary, which is where it can be enforced cheaply.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is unmuting still allowed?
&lt;/h3&gt;

&lt;p&gt;Because a mute can predate the rule. Refusing both directions would make a legacy mute permanent, and "we cannot notify you and you cannot fix it" is not a policy — it is a stuck state.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Get Folcadi&lt;/strong&gt; — a language-exchange messenger with live translation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Official website:&lt;/strong&gt; &lt;a href="https://folcadi.com" rel="noopener noreferrer"&gt;folcadi.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android:&lt;/strong&gt; &lt;a href="https://play.google.com/store/apps/details?id=com.folcadi.app" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows:&lt;/strong&gt; &lt;a href="https://github.com/FrankMilvus/folcadi-releases/releases/latest" rel="noopener noreferrer"&gt;.zip or installer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux:&lt;/strong&gt; &lt;a href="https://github.com/FrankMilvus/folcadi-releases/releases/latest" rel="noopener noreferrer"&gt;.deb or portable tarball&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Articles:&lt;/strong&gt; &lt;a href="https://frankmilvus.github.io/folcadi-articles/" rel="noopener noreferrer"&gt;write-ups and covers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram:&lt;/strong&gt; &lt;a href="https://t.me/frank_says" rel="noopener noreferrer"&gt;@frank_says&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facebook:&lt;/strong&gt; &lt;a href="https://facebook.com/FrankMilvusSays" rel="noopener noreferrer"&gt;facebook.com/FrankMilvusSays&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>showdev</category>
      <category>python</category>
      <category>fastapi</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What Is AI Code Review?</title>
      <dc:creator>Jeff</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:47:52 +0000</pubDate>
      <link>https://dev.to/jeff_pdc/what-is-ai-code-review-47p1</link>
      <guid>https://dev.to/jeff_pdc/what-is-ai-code-review-47p1</guid>
      <description>&lt;p&gt;Quick answer: AI code review is a tool that reads the code you changed, along with a bounded amount of related context, and returns prioritized findings with evidence and a recommendation. It works on top of local git differences and parsers that resolve definitions, callers, and imports. It does not run the application, and it does not prove the code is correct. You assess each finding and decide what to act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually reviews
&lt;/h2&gt;

&lt;p&gt;The input is not a vague scan of the whole repository. It starts with your local git diff, the set of added, removed, and modified lines you are about to ship. On top of that, the review pulls in bounded related context: the definitions a change references, the callers of a function, and the imports that tie files together.&lt;/p&gt;

&lt;p&gt;Each finding comes back with three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Priority&lt;/strong&gt;, so you can tell a blocking defect from a minor note.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence&lt;/strong&gt;, the concrete reason the finding was raised, not just a label.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommendation&lt;/strong&gt;, a suggested next step rather than an automatic edit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The review starts only when you request it. It does not rewrite the file by itself, and it does not approve or merge anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it fits the desktop workflow
&lt;/h2&gt;

&lt;p&gt;When you choose a code folder, the project structure opens first. You can then open a file and see its full source, with views for source only, full comparison, and changes only. Added and removed lines are marked, inline character highlights show exactly what moved, and navigation plus search help you move through a large diff.&lt;/p&gt;

&lt;p&gt;Structure filters add signals that are hard to get by eyeballing a folder:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filter&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Heavily referenced files&lt;/td&gt;
&lt;td&gt;Where a change can ripple across many callers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporary-name candidates&lt;/td&gt;
&lt;td&gt;Files that look like scratch work left in the tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identical content&lt;/td&gt;
&lt;td&gt;Duplicated blocks that may be copies rather than shared logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No incoming references&lt;/td&gt;
&lt;td&gt;Files nothing resolved points to, worth a closer look&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are cues for review, not verdicts. A file with no incoming reference is not automatically safe to delete, and identical text does not mean two pieces of code behave the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evidence model behind the findings
&lt;/h2&gt;

&lt;p&gt;Findings are not all the same kind, and treating them that way leads to bad decisions. The review distinguishes evidence classes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static&lt;/strong&gt; signals such as typecheck, lint, build, and references to an API that does not exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic&lt;/strong&gt; signals from tests, coverage, and smoke runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract&lt;/strong&gt; signals from schema and type drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI hypothesis&lt;/strong&gt;, anything that comes from the model with no deterministic check behind it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A model-only claim is always a hypothesis, never a confirmed defect. When you run project checks, a missing script is reported as unverified rather than passed. If no evidence comes back, the result is inconclusive, because an empty result is not a pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;Powerduck can miss defects or raise false suspicions, and it does not run the app. Dynamic loading, framework entry points, and external consumers can be missed by static analysis. A review narrows where you spend attention and gives you a structured way to read a change, but the call on each finding stays with the engineer.&lt;/p&gt;

&lt;p&gt;You can also export an agent task package as a Markdown handoff that carries the context and revision anchors. Modification is optional, and the receiving agent assesses every suspicion independently rather than trusting the export.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does AI code review replace a human reviewer?&lt;/strong&gt; No. It organizes the diff and surfaces evidence-backed findings, but a person still judges each one and keeps the context that static analysis cannot see, such as product intent and external systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it run my tests or the application?&lt;/strong&gt; It can run detected project commands as explicit checks, but it does not run the app itself. A missing script is marked unverified, not passed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a finding be wrong?&lt;/strong&gt; Yes. The review can raise false suspicions, and model-only claims are hypotheses. Confirm the evidence before you change code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a clean review mean the code is correct?&lt;/strong&gt; No. A review does not prove correctness, and there are documented blind spots including dynamic loading and external consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;AI code review is a focused reading of your diff with bounded context, returning priority, evidence, and a recommendation for each finding. Its value is that it separates static, dynamic, contract, and hypothesis signals so you know how much to trust each note, while keeping the decision in your hands. Treat it as a fast, structured first reviewer that can miss things and can be wrong, not as a correctness certificate. Explore more in the &lt;a href="https://www.powerduck.com/blog/ai-code-review-guide-hub/" rel="noopener noreferrer"&gt;AI code review guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Rotate webhook secrets as Stripe</title>
      <dc:creator>RaccoonDev</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:46:39 +0000</pubDate>
      <link>https://dev.to/mr_raccoon/rotate-webhook-secrets-as-stripe-305c</link>
      <guid>https://dev.to/mr_raccoon/rotate-webhook-secrets-as-stripe-305c</guid>
      <description>&lt;p&gt;When adding a new feature that requires a webhook, you usually think about delivering it, signing it, maybe deduplicating it, and retry logic. But today I want to share one of the best practices for rotating webhook secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main logic of Stripe's secret rerolling
&lt;/h2&gt;

&lt;p&gt;You can choose your secret expiration either immediately or up to 24 hours later.&lt;br&gt;
If you choose the second option, your OLD secret will stay valid for that period, and all webhook messages will be signed with both secrets.&lt;br&gt;
Maybe you will have lots of questions: is it safe? How can we sign the same message with two secrets? And the reasonable one: what is the purpose of it? Now I will answer all of them.&lt;/p&gt;
&lt;h2&gt;
  
  
  What if the webhook secret is compromised? Will a hacker have access for 24 hours?
&lt;/h2&gt;

&lt;p&gt;Of course not. It is easy to confuse a webhook secret with an API key. So I will remind you of the difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A webhook secret is used by the &lt;strong&gt;customer's&lt;/strong&gt; service to check the author of the message. It is validated on &lt;strong&gt;the client side&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;An API key is used by &lt;strong&gt;our&lt;/strong&gt; server to check user permissions, credits, etc. It is validated on &lt;strong&gt;our side&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a hacker obtains a user's webhook secret, the user rerolls it in our service as soon as they notice it and changes it in project settings. The old one is still valid on our side, but the user has already replaced it in project settings, so a forged message signed with the old secret fails verification on their side.&lt;/p&gt;

&lt;p&gt;Keep in mind that until the user updates their settings, the old secret is still accepted there. So after a confirmed leak, choose immediate expiration. The 24-hour window is for planned rotation, not for incident response.&lt;/p&gt;
&lt;h2&gt;
  
  
  How can we deliver a webhook with multiple signatures?
&lt;/h2&gt;

&lt;p&gt;We generate one signature per active secret and put all of them into a single header. The timestamp is part of the signed string, which protects from replay attacks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Webhook-Signature: t=1731038940,v1=&amp;lt;hmac_with_new_secret&amp;gt;,v1=&amp;lt;hmac_with_old_secret&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sender side: sign with every active secret.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secrets&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="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&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="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;whsec_old&lt;/span&gt;&lt;span class="sh"&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="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;whsec_new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;whsec_old&lt;/span&gt;&lt;span class="sh"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Receiver side: it is enough that any signature matches any secret the client knows.&lt;/p&gt;

&lt;p&gt;So a client that is still on the old secret and a client that has already switched to the new one both pass verification with the same message. After 24 hours we simply stop signing with the old secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the purpose of it?
&lt;/h2&gt;

&lt;p&gt;Imagine you are a DevOps engineer who wants to reroll a secret due to basic safety checks, and you create a new secret in the UI, copy it, go to project settings, paste it, and rerun the project. Overall it will take 10-15 minutes, and during this time some messages will fail verification because of mismatched secrets. They will get a 401 and go to the retry queue, but if the retries run out before the new secret is in place, those messages are lost.&lt;/p&gt;

&lt;p&gt;This chance is small, but when you are building software for 20+ companies, you should remember that this may happen sooner or later. With two signatures, the client can switch secrets at any moment inside the window, and nothing is lost.&lt;/p&gt;

&lt;p&gt;Overall, it is just one of the best practices, and it is not always good for you and your projects, so always think before developing.&lt;/p&gt;

</description>
      <category>security</category>
      <category>python</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Best Amazon SES Alternatives in 2026</title>
      <dc:creator>Lalit Kumar Jangid</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:46:17 +0000</pubDate>
      <link>https://dev.to/lalitkumarjangid/best-amazon-ses-alternatives-in-2026-k65</link>
      <guid>https://dev.to/lalitkumarjangid/best-amazon-ses-alternatives-in-2026-k65</guid>
      <description>&lt;p&gt;Amazon SES is built for sending at scale from inside AWS, and many teams start there. The &lt;strong&gt;best Amazon SES alternatives&lt;/strong&gt; are for when you want more around the send: no sandbox approval, a dashboard with logs, templates you can edit without a deploy, delivery events without wiring up Amazon SNS, and marketing email in the same place. This guide compares ten Amazon SES (AWS SES) alternatives on price, free tier, SMTP, webhooks, deliverability and campaigns, using each vendor's own pricing and product pages, checked between October 27 and November 8, 2026.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We make Cresca, so it is listed first. Every other entry is described from the vendor's own public pages, without ranking claims about them. Prices and plans change, so confirm on each vendor's site before you switch.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New SES accounts start in a sandbox: verified recipients only, 200 messages per 24 hours, 1 per second, until AWS grants production access.&lt;/li&gt;
&lt;li&gt;Cresca: a transactional email API and AI-designed marketing campaigns on one plan, from $29/month, with no sandbox.&lt;/li&gt;
&lt;li&gt;Most alternatives include a dashboard, logs and webhooks out of the box; check SMTP, free tiers and how marketing email is billed.&lt;/li&gt;
&lt;li&gt;Still moving off Amazon Pinpoint after its October 30, 2026 end of support? See the &lt;a href="https://www.cresca.xyz/blog/amazon-pinpoint-alternative" rel="noopener noreferrer"&gt;Pinpoint migration guide&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Quick picks&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App email and marketing campaigns on one plan:&lt;/strong&gt; Cresca, from $29/month, no sandbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Largest free monthly allowance in this list:&lt;/strong&gt; Mailtrap (4,000 emails a month) and Resend (3,000 a month).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lowest paid entry price:&lt;/strong&gt; MailerSend at $7/month for 5,000 emails, then Brevo at $9/month from 5,000 emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMTP relay first:&lt;/strong&gt; SMTP2GO, Mailgun and Postmark all offer SMTP next to their APIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is Amazon SES?
&lt;/h2&gt;

&lt;p&gt;Amazon Simple Email Service (Amazon SES, often called AWS SES) is the email sending service inside Amazon Web Services. You send through its API or SMTP interface, pay per email, and build the rest yourself: event handling through other AWS services, templates, a log your support team can read, and any campaign tooling. That trade is what makes SES cheap per email, and it is why teams look for an Amazon SES alternative once they want those parts ready-made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why teams look for an Amazon SES alternative
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The sandbox.&lt;/strong&gt; AWS places all new accounts in the SES sandbox, where you can only send to verified addresses and domains, up to 200 messages per 24 hours and 1 per second. Leaving it means requesting production access; AWS says its support team gives an initial response within 96 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plumbing for events.&lt;/strong&gt; Deliveries, bounces, complaints, opens and clicks are published as events to Amazon SNS, EventBridge, CloudWatch or Data Firehose, which you configure and consume yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing plans.&lt;/strong&gt; New SES accounts start on the Essentials plan at $0.16 per 1,000 emails; Pro and Enterprise plans add fixed monthly charges per account and region. The full breakdown is below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No campaign tools.&lt;/strong&gt; SES sends marketing email, but its pages describe no campaign builder, so newsletters need another tool on top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Pinpoint's end of support&lt;/strong&gt; on October 30, 2026 left teams that used Pinpoint for email campaigns looking for a new home for contacts, templates and campaigns.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Amazon SES pricing: what you actually pay
&lt;/h2&gt;

&lt;p&gt;Amazon SES pricing is per email, with extras for attachments, dedicated IPs and deliverability tools. Figures from the Amazon SES pricing page, checked on November 8, 2026, for the first 10 million emails a month:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Per 1,000 emails&lt;/th&gt;
&lt;th&gt;Fixed monthly fee&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;À la carte&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.10&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Essentials plan&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.16&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pro plan&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.22&lt;/td&gt;
&lt;td&gt;$105 per account per region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enterprise plan&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.23&lt;/td&gt;
&lt;td&gt;$500 per account per region&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Which rate applies:&lt;/strong&gt; AWS says accounts with no metered SES activity since June 1, 2025 start on the Essentials plan from July 21, 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attachments:&lt;/strong&gt; $0.12 per GB of attachment data sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated IPs:&lt;/strong&gt; $24.95 per IP per month (standard), or managed dedicated IPs at $15 per account per month plus $0.08 per 1,000 emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual Deliverability Manager:&lt;/strong&gt; $0.07 per 1,000 emails for SES deliverability insights.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tier:&lt;/strong&gt; new AWS customers get up to $200 in AWS Free Tier credits that can be used on SES.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At 10,000 emails a month, that is about $1.00 à la carte or $1.60 on Essentials, and $26.60 if you add one standard dedicated IP. The per-email price is rarely the reason teams switch. The cost is the work around it: the sandbox request, wiring delivery events through Amazon SNS or EventBridge, a template workflow, and a place where support can see what happened to an email.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we compared these Amazon SES alternatives
&lt;/h2&gt;

&lt;p&gt;Each service is described from its own pricing pages and documentation, not from third-party reviews. We looked at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Setup:&lt;/strong&gt; whether you can send to real recipients on day one, or wait for an approval step like the SES sandbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; the free option and the cheapest paid plan, in US dollars per month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending options:&lt;/strong&gt; HTTP API, SMTP relay and delivery webhooks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deliverability:&lt;/strong&gt; domain authentication (SPF, DKIM, DMARC) and how bounces and complaints are handled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing email:&lt;/strong&gt; whether campaigns live in the same account and plan, or in a separate product.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best Amazon SES alternatives compared (2026)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Free option&lt;/th&gt;
&lt;th&gt;Paid from&lt;/th&gt;
&lt;th&gt;SMTP relay&lt;/th&gt;
&lt;th&gt;Webhooks&lt;/th&gt;
&lt;th&gt;Marketing email&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cresca&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;App email and AI-designed marketing campaigns in one account&lt;/td&gt;
&lt;td&gt;30 days free, 1,000 emails, no credit card&lt;/td&gt;
&lt;td&gt;$29/month: 7,500 emails and 5,000 contacts, campaigns included&lt;/td&gt;
&lt;td&gt;On the roadmap&lt;/td&gt;
&lt;td&gt;On the roadmap&lt;/td&gt;
&lt;td&gt;Yes, on the same plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developers; sends emails built with React Email&lt;/td&gt;
&lt;td&gt;Free plan: 3,000 emails/month, 100 per day&lt;/td&gt;
&lt;td&gt;$20/month for 50,000 emails&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Separate marketing plans priced by contacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Postmark&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Transactional email with separate message streams&lt;/td&gt;
&lt;td&gt;100 emails/month developer plan&lt;/td&gt;
&lt;td&gt;$15/month for 10,000 emails&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Broadcast message streams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SendGrid&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Email API at scale, part of Twilio&lt;/td&gt;
&lt;td&gt;60-day free trial, 100 emails/day&lt;/td&gt;
&lt;td&gt;Essentials from $19.95/month&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Marketing Campaigns, sold as a separate plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mailgun&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developers; email API, SMTP and email validation&lt;/td&gt;
&lt;td&gt;Free plan: 100 emails/day&lt;/td&gt;
&lt;td&gt;$15/month for 10,000 emails&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not stated on its pricing page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MailerSend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Email, SMS and WhatsApp for apps&lt;/td&gt;
&lt;td&gt;Free plan: 500 emails/month&lt;/td&gt;
&lt;td&gt;$7/month for 5,000 emails&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not stated; bulk emailing on paid plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mailtrap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developer and product teams; sending plus a testing sandbox&lt;/td&gt;
&lt;td&gt;Free plan: 4,000 emails/month, 150 per day&lt;/td&gt;
&lt;td&gt;$15/month for 10,000 emails&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Email Marketing, sold separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mailchimp Transactional&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mailchimp customers adding app email&lt;/td&gt;
&lt;td&gt;Up to 500 free sends to a verified domain&lt;/td&gt;
&lt;td&gt;$20 per block of 25,000 emails, plus a Mailchimp Standard or Premium plan&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, through the Mailchimp plan it is added to&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Brevo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Marketing campaigns, automation and transactional email in one account&lt;/td&gt;
&lt;td&gt;Free plan&lt;/td&gt;
&lt;td&gt;Starter $9/month from 5,000 emails&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, in the same account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SMTP2GO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SMTP relay and email API, with SMS&lt;/td&gt;
&lt;td&gt;Free plan: 1,000 emails/month&lt;/td&gt;
&lt;td&gt;$10/month for 10,000 emails&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Not stated on its pricing page&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Based on each vendor's public pricing and product pages, checked on October 27, 2026 (Brevo and SMTP2GO on November 8, 2026). Prices in US dollars, billed monthly where both options are shown.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Cresca: SES-level sending without the setup, plus campaigns
&lt;/h2&gt;

&lt;p&gt;Cresca is a transactional email service and an AI email marketing platform in one account. Verify your domain, create an API key and send: there is no sandbox to leave, and the dashboard shows a timeline for every email. See &lt;a href="https://www.cresca.xyz/transactional-email" rel="noopener noreferrer"&gt;Cresca transactional email&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API:&lt;/strong&gt; &lt;code&gt;POST /emails&lt;/code&gt; for one email to up to 50 recipients, batches of up to 100, scheduling up to 30 days ahead, attachments, tags and &lt;code&gt;Idempotency-Key&lt;/code&gt; headers for safe retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Templates:&lt;/strong&gt; Design in a template editor, publish, and send by alias with variables. Every published version is kept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing:&lt;/strong&gt; AI-designed campaigns, contact lists, scheduling and campaign reports on the same plan and the same verified domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing:&lt;/strong&gt; 30 days free with 1,000 emails and no credit card. Professional is $29/month for 7,500 emails and 5,000 contacts, covering API email and campaigns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On the roadmap:&lt;/strong&gt; Outbound webhooks and an SMTP relay. Today you read each email's status from the API or the dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Resend
&lt;/h2&gt;

&lt;p&gt;Resend presents itself as an email API for developers and highlights emails built with React Email.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free option:&lt;/strong&gt; 3,000 emails per month, up to 100 per day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid:&lt;/strong&gt; Pro is $20/month for 50,000 emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending:&lt;/strong&gt; REST API, SMTP relay, SDKs and webhooks on all plans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing:&lt;/strong&gt; Separate marketing plans priced by contacts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Postmark
&lt;/h2&gt;

&lt;p&gt;Postmark describes itself as a transactional email service, with transactional and broadcast email kept in separate message streams.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free option:&lt;/strong&gt; A developer plan with 100 emails per month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid:&lt;/strong&gt; Basic is $15/month for 10,000 emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending:&lt;/strong&gt; Email API, SMTP, event webhooks and inbound processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing:&lt;/strong&gt; Broadcast message streams in the same account.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. SendGrid
&lt;/h2&gt;

&lt;p&gt;SendGrid, part of Twilio, sells its Email API and Marketing Campaigns as separate products.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free option:&lt;/strong&gt; A 60-day free trial with 100 emails per day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid:&lt;/strong&gt; Email API Essentials starts at $19.95/month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending:&lt;/strong&gt; API and SMTP, with event webhooks on paid plans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing:&lt;/strong&gt; Marketing Campaigns, purchased as a separate plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Mailgun
&lt;/h2&gt;

&lt;p&gt;Mailgun calls itself a transactional email delivery service and API for developers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free option:&lt;/strong&gt; 100 emails per day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid:&lt;/strong&gt; Basic starts at $15/month for 10,000 emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending:&lt;/strong&gt; RESTful APIs and SMTP relay, with webhooks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing:&lt;/strong&gt; Not stated on its pricing page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. MailerSend
&lt;/h2&gt;

&lt;p&gt;MailerSend presents itself as a team-friendly email API for apps, with SMS and WhatsApp alongside email.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free option:&lt;/strong&gt; 500 emails per month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid:&lt;/strong&gt; Hobby is $7/month for 5,000 emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending:&lt;/strong&gt; Email API, SMTP relay and webhooks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing:&lt;/strong&gt; No separate marketing product described; bulk emailing on paid plans.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Mailtrap
&lt;/h2&gt;

&lt;p&gt;Mailtrap offers email sending for developer and product teams, with email marketing and a testing sandbox as separate products.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free option:&lt;/strong&gt; 4,000 emails per month, up to 150 per day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid:&lt;/strong&gt; Basic is $15/month for 10,000 emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending:&lt;/strong&gt; API, SMTP relay and webhooks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing:&lt;/strong&gt; Email Marketing, sold separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Mailchimp Transactional (formerly Mandrill)
&lt;/h2&gt;

&lt;p&gt;Mailchimp Transactional is sold as an add-on to a Mailchimp plan.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free option:&lt;/strong&gt; Up to 500 free emails to a verified domain for new users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid:&lt;/strong&gt; $20 per block of 25,000 emails, plus a Mailchimp Standard or Premium plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending:&lt;/strong&gt; API, SMTP integration and webhooks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing:&lt;/strong&gt; Through the Mailchimp plan it is added to.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. Brevo
&lt;/h2&gt;

&lt;p&gt;Brevo sells email marketing, automation and transactional email in one account, alongside SMS and other channels.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free option:&lt;/strong&gt; A free plan, with no credit card needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid:&lt;/strong&gt; Starter is $9/month from 5,000 emails a month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending:&lt;/strong&gt; SMTP relay and API keys for transactional email, with transactional webhooks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing:&lt;/strong&gt; Campaigns, templates and segmentation on the same plans; automation from the Standard plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. SMTP2GO
&lt;/h2&gt;

&lt;p&gt;SMTP2GO describes itself as an email and SMS delivery service that you connect through SMTP or its API.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free option:&lt;/strong&gt; Up to 1,000 emails a month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid:&lt;/strong&gt; Starter is $10/month for 10,000 emails, with extra emails at $1 per thousand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sending:&lt;/strong&gt; SMTP relay and HTTP API, with webhooks and reporting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketing:&lt;/strong&gt; Not stated on its pricing page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deliverability: what changes when you leave Amazon SES
&lt;/h2&gt;

&lt;p&gt;Switching providers does not reset your domain's reputation, but it does change who signs and sends your mail. Plan for these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication records.&lt;/strong&gt; Add the new provider's DKIM records and, where it uses a custom MAIL FROM domain, its SPF record. Keep your DMARC record; a domain can only have one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared or dedicated IPs.&lt;/strong&gt; Most plans in this list send from shared IPs. A dedicated IP needs steady volume and a warm-up period before it helps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounces and complaints.&lt;/strong&gt; Carry your SES suppression list over, so addresses that bounced or complained are not mailed again from the new provider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradual moves.&lt;/strong&gt; Switch one email type at a time and watch bounce and complaint rates before moving the next.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Cresca, domain setup shows the DKIM and SPF records to publish and checks whether your domain has a DMARC record. Hard bounces, repeated soft bounces, complaints and unsubscribes are suppressed automatically, for API email and campaigns alike. See the &lt;a href="https://www.cresca.xyz/blog/email-deliverability-complete-guide" rel="noopener noreferrer"&gt;email deliverability guide&lt;/a&gt; for the full checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving from Amazon SES to Cresca
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verify your sending domain&lt;/strong&gt; in Cresca by publishing the DNS records it shows, and keep SES sending while you switch. See the &lt;a href="https://www.cresca.xyz/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a sending-access API key&lt;/strong&gt; and store it as &lt;code&gt;CRESCA_API_KEY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move templates.&lt;/strong&gt; Paste your SES template HTML into Cresca's template editor with Import HTML, mark variables with triple braces and publish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap the send call&lt;/strong&gt;, one email type at a time, starting with a low-risk one such as a welcome email.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a list of addresses that bounced or complained on SES&lt;/strong&gt; and leave them out of your Cresca sends, so your new domain setup starts with a clean reputation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch the rest&lt;/strong&gt; once the dashboard timeline looks right, then retire the SES configuration.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the same email with the AWS SDK for JavaScript (SESv2) and with Cresca:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: Amazon SES with the AWS SDK for JavaScript v3&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SESv2Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SendEmailCommand&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;@aws-sdk/client-sesv2&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;ses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SESv2Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SendEmailCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;FromEmailAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Acme &amp;lt;hello@acme.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ToAddresses&lt;/span&gt;&lt;span class="p"&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;ana@example.com&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;Content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Simple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Welcome to Acme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Html&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;Hi Ana, your account is ready.&amp;lt;/p&amp;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="p"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After: Cresca, with fetch (Node.js 18+)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.cresca.xyz/api/v1/emails&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&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="nx"&gt;CRESCA_API_KEY&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-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;application/json&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;Idempotency-Key&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;welcome/user-123&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&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;Acme &amp;lt;hello@acme.com&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ana@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Welcome to Acme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;Hi Ana, your account is ready.&amp;lt;/p&amp;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="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;Full guides for your stack: &lt;a href="https://www.cresca.xyz/blog/send-email-nodejs" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt;, &lt;a href="https://www.cresca.xyz/blog/send-email-python" rel="noopener noreferrer"&gt;Python&lt;/a&gt;, &lt;a href="https://www.cresca.xyz/blog/send-email-java" rel="noopener noreferrer"&gt;Java&lt;/a&gt;, &lt;a href="https://www.cresca.xyz/blog/send-email-golang" rel="noopener noreferrer"&gt;Go&lt;/a&gt;, &lt;a href="https://www.cresca.xyz/blog/send-email-php" rel="noopener noreferrer"&gt;PHP&lt;/a&gt; and more. Comparing transactional providers more broadly? See the &lt;a href="https://www.cresca.xyz/blog/best-transactional-email-services" rel="noopener noreferrer"&gt;best transactional email services&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the best Amazon SES alternative?
&lt;/h3&gt;

&lt;p&gt;It depends on what you need around sending. If you also send newsletters and campaigns, Cresca puts a transactional email API and AI-designed marketing email on one plan, with no sandbox and a dashboard for every email. Resend, Postmark, SendGrid, Mailgun, MailerSend, Mailtrap, Mailchimp Transactional and SMTP2GO focus on sending, and Brevo combines marketing with transactional email.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a cheaper alternative to Amazon SES?
&lt;/h3&gt;

&lt;p&gt;Per email, rarely. Amazon SES costs $0.10 per 1,000 emails à la carte or $0.16 on the Essentials plan, which is less per email than the paid plans in this list. Alternatives charge more per email but include the dashboard, logs, templates, delivery events and support that you would otherwise build or buy around SES. Several also have free plans, from 1,000 emails a month on SMTP2GO to 4,000 on Mailtrap.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does it take to get out of the Amazon SES sandbox?
&lt;/h3&gt;

&lt;p&gt;You request production access from the SES console or the AWS CLI, giving your use case and website. AWS says its support team provides an initial response within 96 hours. Until then you can only send to verified addresses, up to 200 messages per 24 hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Amazon SES affected by the Amazon Pinpoint end of support?
&lt;/h3&gt;

&lt;p&gt;No. Amazon Pinpoint's engagement features ended on October 30, 2026, but Amazon SES continues. Teams that used Pinpoint for email campaigns, segments and journeys need another place for them, which is what the Pinpoint migration guide covers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I keep my domain authentication when I leave SES?
&lt;/h3&gt;

&lt;p&gt;You keep your domain, but each provider signs mail with its own DKIM keys, so you add the new provider's DNS records. DKIM records for different providers can sit side by side, which lets you move one email type at a time and keep SES running until the switch is complete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do SES alternatives support SMTP?
&lt;/h3&gt;

&lt;p&gt;Most of the services in this comparison offer an SMTP relay as well as an HTTP API. Cresca sends through its HTTP API today, and an SMTP relay is on its roadmap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Cresca
&lt;/h2&gt;

&lt;p&gt;Send your app's email and your campaigns from one account, with no sandbox. &lt;strong&gt;&lt;a href="https://app.cresca.xyz/signup" rel="noopener noreferrer"&gt;Try Cresca free for 30 days&lt;/a&gt;&lt;/strong&gt;, no credit card required, then follow the &lt;a href="https://www.cresca.xyz/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>email</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>What Is Agent2Agent (A2A)?</title>
      <dc:creator>Jeff</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:46:08 +0000</pubDate>
      <link>https://dev.to/jeff_pdc/what-is-agent2agent-a2a-53kj</link>
      <guid>https://dev.to/jeff_pdc/what-is-agent2agent-a2a-53kj</guid>
      <description>&lt;p&gt;Quick answer: Agent2Agent (A2A) is an open protocol that lets one agent communicate with another over JSON-RPC. It defines an agent card that describes the remote service, a set of task methods for sending work and reading or cancelling it, and optional streaming over SSE. The desktop can verify a card, exercise the methods, and generate a local A2A server, but A2A is a supporting capability rather than the core of the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem A2A addresses
&lt;/h2&gt;

&lt;p&gt;Without a shared protocol, every agent integration is bespoke. One agent calls another through a custom shape, parses a private response, and has to be rebuilt the moment the other side changes. There is no standard way to describe what the agent can do, track a task that takes time, or cancel work already in flight.&lt;/p&gt;

&lt;p&gt;A2A fixes those points with a small, task-based contract so agents from different systems can interoperate without a point-to-point design.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core pieces
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent card&lt;/td&gt;
&lt;td&gt;Describes the agent's skills, endpoints, and security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON-RPC methods&lt;/td&gt;
&lt;td&gt;Carry the task requests and responses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task&lt;/td&gt;
&lt;td&gt;The unit of work sent to the remote agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSE streaming&lt;/td&gt;
&lt;td&gt;Optional channel for progress and status updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cancel&lt;/td&gt;
&lt;td&gt;A method to stop a task that is still running&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The card is the entry point. A caller fetches it first, reads what the agent offers and how it is secured, then uses the task methods rather than guessing at the shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a task moves
&lt;/h2&gt;

&lt;p&gt;A2A is built around a task that can take time rather than a single immediate response.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The caller fetches and verifies the agent card.&lt;/li&gt;
&lt;li&gt;It sends a task using the declared method.&lt;/li&gt;
&lt;li&gt;The remote agent returns a task reference and status.&lt;/li&gt;
&lt;li&gt;The caller polls the status or subscribes over SSE.&lt;/li&gt;
&lt;li&gt;It can cancel the task before it reaches a terminal state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because the work is long-running, the status is a first-class part of the protocol rather than something the caller invents. A task moves through defined states, and the final result is read separately from the initial acceptance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where security sits
&lt;/h2&gt;

&lt;p&gt;The agent card carries the security information, and the protocol supports signed requests. A JWS signature lets the caller verify the message, but signature checks have limits, and a valid signature does not make the payload correct or safe. Treat the signature as an authentication and integrity check, not as proof the task behavior is right.&lt;/p&gt;

&lt;p&gt;The desktop can check a card locally, including its structure and security fields, before you trust the remote agent. It also handles the case where a JSON-RPC error arrives inside an HTTP 200, which a status-only check would miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest scope
&lt;/h2&gt;

&lt;p&gt;A2A is a supporting capability behind the two core workflows. It matters because agents increasingly call one another, and those calls need the same kind of verification as any other API, but it is not a reason to position the product as a general client. A generated A2A server is a local scaffold for testing, not a production deployment, and it does not replace the real remote agent.&lt;/p&gt;

&lt;p&gt;Powerduck treats A2A as another surface to verify, with the same evidence and idempotency discipline used across the rest of the desktop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is A2A a transport or a full protocol?&lt;/strong&gt; It is a protocol over JSON-RPC, with an agent card, task methods, status, and optional SSE streaming, rather than just a wire format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is the agent card fetched first?&lt;/strong&gt; It describes the skills, endpoints, and security so the caller knows how to interact before sending a task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a valid signature mean the task is safe?&lt;/strong&gt; No. A JWS signature checks integrity and origin, not the correctness of the payload or the behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;A2A is an open, task-based protocol for agents to communicate over JSON-RPC, built around an agent card, task methods with status, and optional SSE streaming and cancellation. It removes the need for bespoke point-to-point integrations while still requiring real verification, because a signed or accepted task is not a verified behavior. Keep it in its lane as a supporting capability, and verify the card and the responses the same way you would any API. Read more in the &lt;a href="https://www.powerduck.com/blog/a2a-guide-hub/" rel="noopener noreferrer"&gt;A2A guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Is Shopify Down? How to Check Shopify Status and What to Do Next</title>
      <dc:creator>Lavkush Kumar</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:45:35 +0000</pubDate>
      <link>https://dev.to/kushkrg/is-shopify-down-how-to-check-shopify-status-and-what-to-do-next-5140</link>
      <guid>https://dev.to/kushkrg/is-shopify-down-how-to-check-shopify-status-and-what-to-do-next-5140</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://codingcush.com/is-shopify-down-how-to-check" rel="noopener noreferrer"&gt;CodingCush&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If your Shopify store suddenly will not load, the fastest way to know whether Shopify itself is down is to open the official Shopify Status page at shopifystatus.com: if it says "All Systems Operational" with green ticks, Shopify is up and the problem is almost certainly in your store, your internet connection or your browser; if it shows a partial or major outage for Admin, Checkout or Storefront, the problem is on Shopify's side and the only real fix is to wait while Shopify repairs it. Checking this first takes twenty seconds and saves hours of changing settings that were never broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Check the Official Shopify Status Page
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://www.shopifystatus.com/" rel="noopener noreferrer"&gt;the official Shopify Status page at shopifystatus.com&lt;/a&gt; in your browser. You will see a simple dashboard that reports each part of Shopify separately: Admin, Checkout, Storefront, API and Mobile, Reports and Dashboards, Support, Point of Sale and third-party services. A green tick means that part is working normally. A yellow or orange mark means degraded performance or a partial outage, and a red cross means a major outage.&lt;/p&gt;

&lt;p&gt;Read the page like a weather report for your store. If Checkout shows a major outage but Storefront is green, visitors can browse your products but cannot pay, which is urgent for sales but not something you can repair yourself. If everything is green and your store still fails for you, the fault is local, and the rest of this guide is your checklist. Shopify also offers a personalised "My Shopify Status" view once you log in, which shows incidents that affect your specific store, because a problem can touch a small group of stores without appearing as a global outage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgncodkjwp371cls83a4.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgncodkjwp371cls83a4.png" alt="The official Shopify Status page at shopifystatus.com showing All Systems Operational with green ticks for Admin, Checkout, Storefront and Support" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The official Shopify Status page at shopifystatus.com, showing "All Systems Operational" with green ticks for Admin, Checkout, Storefront, Reports and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Rule Out Your Own Connection and Browser
&lt;/h2&gt;

&lt;p&gt;Before blaming any platform, test the boring local causes, because they are the most common ones. Open your store in a private or incognito window, which ignores your saved cookies and extensions. Try a different browser, and then a different network, for example your phone on mobile data instead of your home Wi-Fi. Ask a friend in another city to open the store, or check it on your phone with Wi-Fi switched off.&lt;/p&gt;

&lt;p&gt;If the store opens everywhere except on your laptop, Shopify was never down: your browser cache, an aggressive ad blocker, a VPN or your internet provider's DNS is the culprit. Clear the cache, disable the VPN for a minute and try again. If the store fails on every device and every network, move to the next step, because now a store-level or platform-level problem is likely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Check Whether It Is Your Whole Store or One Part
&lt;/h2&gt;

&lt;p&gt;"Shopify is down" can mean four different problems, and each has a different owner. Your storefront may fail while the admin works, which points at your theme, a newly installed app or a domain problem. Your admin may fail while the storefront sells happily, which points at a login, browser or account issue. Checkout may fail while everything else works, which is the pattern in our guide to &lt;a href="https://codingcush.com/shopify-checkout-not-working-fixes" rel="noopener noreferrer"&gt;Shopify checkout not working and how to fix it&lt;/a&gt;, with payment and settings checks in order. Or everything may fail at once, which is either a real platform outage or your domain expiring, and the status page plus a quick domain check will separate those two quickly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What fails?&lt;/th&gt;
&lt;th&gt;Most likely cause&lt;/th&gt;
&lt;th&gt;First move&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Everything, for everyone&lt;/td&gt;
&lt;td&gt;Platform outage or expired domain&lt;/td&gt;
&lt;td&gt;Status page, then domain expiry check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storefront only&lt;/td&gt;
&lt;td&gt;Theme change, app conflict, DNS&lt;/td&gt;
&lt;td&gt;Preview a fresh theme, undo recent changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Admin only&lt;/td&gt;
&lt;td&gt;Browser, cache, login session&lt;/td&gt;
&lt;td&gt;Incognito window, different browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout only&lt;/td&gt;
&lt;td&gt;Payments paused, test mode, outage&lt;/td&gt;
&lt;td&gt;Check Shopify Payments and test mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only for you&lt;/td&gt;
&lt;td&gt;Your network, VPN or DNS&lt;/td&gt;
&lt;td&gt;Mobile data test, clear cache&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 4: If It Is a Real Outage, Do These Things While You Wait
&lt;/h2&gt;

&lt;p&gt;A genuine Shopify outage feels helpless, but there is still useful work to do. First, do not start editing your theme, deleting apps or changing DNS settings in a panic: when the platform recovers, those hurried changes become brand-new problems of your own making. Second, note the time the problem started and take a screenshot of the status page; if you later talk to support or to customers, exact times beat vague memories. Third, tell your customers quickly and calmly on social media or by email that checkout is temporarily unavailable and that you will announce when it is back. Shoppers forgive outages; they do not forgive silence.&lt;/p&gt;

&lt;p&gt;Fourth, keep refreshing the status page rather than your store, because Shopify updates incident notes there as engineers work. Most platform incidents are resolved within a few hours. When the status returns to green, place a small test order on your own store to confirm that browsing, checkout and payment all work end to end before you tell customers the good news.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: If the Status Is Green but Your Store Is Still Broken
&lt;/h2&gt;

&lt;p&gt;A green status page with a broken store means the problem lives in your account. Work backwards through your most recent changes, because new problems usually follow new actions. Did you install an app today? Disable it temporarily. Did you edit theme code? Roll back to the previous version from the theme editor's version history. Did you connect or transfer a domain? Check that the DNS records still point to Shopify and that the domain has not expired at your registrar. Did payments stop? Open Settings, then Payments, and look for a paused or restricted Shopify Payments banner, which can follow an identity review.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://help.shopify.com/" rel="noopener noreferrer"&gt;official Shopify Help Center&lt;/a&gt; is the right next stop after these checks, with guided diagnostics and 24/7 support chat for account-specific faults that a public status page cannot see. And if several unrelated things in your store feel broken at once, our checklist of &lt;a href="https://codingcush.com/common-shopify-problems-and-how-to-fix-them" rel="noopener noreferrer"&gt;common Shopify problems and how to fix them&lt;/a&gt; walks through the usual suspects one by one, from slow pages to missing products.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Get Warned Earlier Next Time
&lt;/h2&gt;

&lt;p&gt;Once this scare is over, make the next one shorter. Bookmark the status page on your phone, not just your laptop, because outages rarely wait for office hours. Turn on notifications in any community or status-alert service you already use, and keep a simple "store down" draft message ready for your social channels so you are writing calmly in advance instead of typing in a panic. Finally, keep a short note of who controls what: domain registrar login, Shopify login and your backup admin's email. In a real emergency, hunting for passwords wastes the very minutes you are trying to save.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How can I check if Shopify is down right now?
&lt;/h3&gt;

&lt;p&gt;Open shopifystatus.com. Green ticks and "All Systems Operational" mean Shopify is up. Orange or red marks next to Admin, Checkout or Storefront mean Shopify has a live incident affecting that part.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is my Shopify store not loading but the status page is green?
&lt;/h3&gt;

&lt;p&gt;Then the fault is local to your store or your connection: a theme or app change, a domain or DNS problem, a paused payment account, or simply your browser cache or network. Test in an incognito window and on mobile data to narrow it down.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long do Shopify outages usually last?
&lt;/h3&gt;

&lt;p&gt;Most incidents are resolved within a few hours, and many within the hour. The status page posts progress updates during an incident, so check there instead of repeatedly refreshing your store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I fix a Shopify outage myself?
&lt;/h3&gt;

&lt;p&gt;No. A platform outage is repaired by Shopify's engineers. Your job during an outage is to avoid making risky changes, inform your customers, and test a full order once the status returns to green.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should I do if only checkout is down?
&lt;/h3&gt;

&lt;p&gt;Check the status page's Checkout row first. If it is green, inspect your own payment settings: confirm Shopify Payments is active, test mode is off, and no verification banner is waiting in Settings, then Payments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When your store fails, do not guess and do not panic-edit. Spend twenty seconds on the official status page, test on another device and network, work out which part is actually failing, and only then fix the thing that is yours to fix. That calm order of checks turns a frightening "Shopify is down" moment into a short, clear diagnosis almost every time.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>ecommerce</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What AI Code Review Cannot Catch</title>
      <dc:creator>Jeff</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:44:34 +0000</pubDate>
      <link>https://dev.to/jeff_pdc/what-ai-code-review-cannot-catch-4ipk</link>
      <guid>https://dev.to/jeff_pdc/what-ai-code-review-cannot-catch-4ipk</guid>
      <description>&lt;p&gt;Quick answer: AI code review cannot see code that is reached through dynamic loading or framework entry points, external consumers outside the repository, or the runtime behavior of the application, because it does not run the app. It can also miss defects or raise false suspicions, and it does not prove correctness. Knowing these blind spots tells you which parts still need a test, a smoke run, or a person's judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The structural blind spots
&lt;/h2&gt;

&lt;p&gt;The review is built on local git differences plus parsers that resolve definitions, callers, and imports. Anything outside that resolved graph can be missed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Blind spot&lt;/th&gt;
&lt;th&gt;Why it is missed&lt;/th&gt;
&lt;th&gt;How to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic loading&lt;/td&gt;
&lt;td&gt;Code is reached at runtime, not by static resolution&lt;/td&gt;
&lt;td&gt;Tests or smoke runs that exercise the path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework entry points&lt;/td&gt;
&lt;td&gt;The framework wires the call outside the parsed code&lt;/td&gt;
&lt;td&gt;Run the app and trace the entry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External consumers&lt;/td&gt;
&lt;td&gt;Callers live outside the repository&lt;/td&gt;
&lt;td&gt;Integration checks and service knowledge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime behavior&lt;/td&gt;
&lt;td&gt;The review never executes the app&lt;/td&gt;
&lt;td&gt;Dynamic tests and smoke calls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A file with no incoming reference is a cue, not a verdict. It is not automatically safe to delete, because an external consumer or dynamic loader may be the real caller. Identical text does not prove interchangeable behavior either, since two matching blocks can sit in different runtime contexts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What static analysis cannot determine
&lt;/h2&gt;

&lt;p&gt;Some questions only have answers once code runs. Static reading cannot tell you whether a branch behaves correctly under real data, whether a side effect fires in the right order, or how a change interacts with a live service. That is why dynamic evidence from tests, coverage, and smoke runs is a separate class rather than an assumption.&lt;/p&gt;

&lt;p&gt;A dynamic run still only covers the paths it reaches, so an unexercised branch carries no evidence. When a project script is missing, the check is unverified rather than passed, and an empty result is inconclusive, not a green check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reasoning limits
&lt;/h2&gt;

&lt;p&gt;Model-only notes are AI hypotheses, and they can be wrong. The model does not know product intent it has not been given, and it can raise a false suspicion that reads as confident. A model-only claim is always a hypothesis until you reproduce it or trace it to a real definition.&lt;/p&gt;

&lt;p&gt;The deterministic verdict protects you in one direction: failing evidence fails, and no verified evidence is inconclusive. It cannot, however, invent evidence for paths the tooling cannot see. A clean review means the checks that ran did not fail, not that every possible path is sound.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to cover the blind spots
&lt;/h2&gt;

&lt;p&gt;Use the review for what it does well, then close the gaps deliberately.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run dynamic tests and smoke calls to cover behavior static analysis cannot reach.&lt;/li&gt;
&lt;li&gt;Exercise framework entry points and dynamic loading paths explicitly.&lt;/li&gt;
&lt;li&gt;Map external consumers so unused-looking files are not removed by mistake.&lt;/li&gt;
&lt;li&gt;Keep a person on design and product-intent decisions.&lt;/li&gt;
&lt;li&gt;Re-run the review after adding checks so the verdict updates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Powerduck states these limits rather than hiding them, and it keeps each finding tied to reproducible evidence so you can tell what was verified and what remains unknown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does a clean review mean the code is safe to ship?&lt;/strong&gt; No. It means the checks that ran did not fail. Dynamic loading, external consumers, and runtime behavior can still hide defects, and the review does not prove correctness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I delete a file with no incoming references?&lt;/strong&gt; Not on that evidence alone. Dynamic loading or an external consumer may call it. Confirm there is no runtime or outside caller first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the tool run the app to catch runtime defects?&lt;/strong&gt; No. It does not run the app. Use tests and smoke runs for runtime behavior, and treat missing scripts as unverified.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;AI code review is a strong structural reader, but it has fixed blind spots: dynamic loading, framework entry points, external consumers, and runtime behavior are all outside its static view, and model-only notes can be wrong. Cover those with dynamic tests, smoke runs, and human judgment, and never read a clean review as a correctness certificate. The tool narrows your attention; it does not remove the need to verify what it cannot see. More in the &lt;a href="https://www.powerduck.com/blog/ai-code-review-guide-hub/" rel="noopener noreferrer"&gt;AI code review guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>23 of 56 Pages Ship No JavaScript: The Guide Fleet Is Static, the App Fleet Is Not</title>
      <dc:creator>OnePassApps</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:41:56 +0000</pubDate>
      <link>https://dev.to/onepassapps/23-of-56-pages-ship-no-javascript-the-guide-fleet-is-static-the-app-fleet-is-not-1dkm</link>
      <guid>https://dev.to/onepassapps/23-of-56-pages-ship-no-javascript-the-guide-fleet-is-static-the-app-fleet-is-not-1dkm</guid>
      <description>&lt;p&gt;I work on OnePassApps, so this is our own site, and every number below was measured on today's build over plain HTTPS from outside the browser - no session, no cookies, no JavaScript.&lt;/p&gt;

&lt;p&gt;Half of our own sitemap ships our application code and half does not, and the line falls in an odd place. Of the 56 URLs in our sitemap, 33 load the app bundle and 23 do not - and the 23 are the guides index plus all 22 individual guide pages. Those are the only routes on the site where a reader gets the whole document with nothing of ours running on top of it.&lt;/p&gt;

&lt;p&gt;This is a measurement of the layer between the server and the browser: what each route asks the browser to fetch, and what it leaves out.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the sitemap splits
&lt;/h2&gt;

&lt;p&gt;Our sitemap answers HTTP 200 with 4,046 bytes and 56 URLs. Reading every one of them and counting the script tags in the served HTML gives two clean groups.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;URLs&lt;/th&gt;
&lt;th&gt;script tags&lt;/th&gt;
&lt;th&gt;of them ours&lt;/th&gt;
&lt;th&gt;modulepreload links&lt;/th&gt;
&lt;th&gt;link tags&lt;/th&gt;
&lt;th&gt;root div&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App bundle&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No app bundle&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 23 routes with no bundle are /guides, /guides/newsflow, /guides/movie-tracker and the 19 other guide pages. Every one of the 33 routes with a bundle is an app page, a Studio page, or one of the four other routes - the home page, the vote board, the plans page and the terms page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 23 routes actually ship
&lt;/h2&gt;

&lt;p&gt;Three script tags each, and only one of them is ours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the JSON-LD block, the single script of ours, carrying the structured data&lt;/li&gt;
&lt;li&gt;Cloudflare's email-obfuscation helper&lt;/li&gt;
&lt;li&gt;the Cloudflare analytics beacon&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no app entry, no fallback script and no preload hint. The modulepreload links are the clearest marker of the difference: the 33 routes that boot the app carry 3 of them, pointing at the runtime, the shared shell and the React vendor chunk, and the 23 routes that do not boot the app carry none. The string modulepreload does not appear anywhere in the 23 served pages.&lt;/p&gt;

&lt;p&gt;They also drop the empty mount point: the 0 of 23 that carry a root div element are the app routes, and the guide pages carry the static copy instead. The SEO fallback block that the app routes carry is missing from all 23 of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things they still take
&lt;/h2&gt;

&lt;p&gt;Dropping the JavaScript does not make the guide pages cheap, because they keep both stylesheets - every one of the 56 pages links the same two: shell-fallback.css and index-B_evf03w.css. The second one is 242,648 bytes raw and 40,833 bytes under brotli, so a reader who opens one guide page still downloads the whole application stylesheet.&lt;/p&gt;

&lt;p&gt;The theme is not lost either. All 56 pages arrive with the html element already carrying data-theme="dark" and data-theme-preset="graphite", so the guide pages are dark before the first paint without any script to do it. The app routes additionally run a 1,316-byte inline script that forces that theme and drops a stored light preference; the guide pages have no such script, because they do not need one. The startup-shell marker is on all 56 pages either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the app routes pull instead
&lt;/h2&gt;

&lt;p&gt;The 33 routes that boot the app point at five hashed files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/assets/app-shell-shared-DCrZF6q_.js - 369,609 bytes raw, 116,569 bytes under brotli&lt;/li&gt;
&lt;li&gt;/assets/index-B_evf03w.css - 242,648 bytes raw, 40,833 bytes under brotli&lt;/li&gt;
&lt;li&gt;/assets/index-LNYK_bk0.js - 324,591 bytes raw, 90,675 bytes under brotli&lt;/li&gt;
&lt;li&gt;/assets/react-vendor-DY6N_LXH.js - 178,924 bytes raw, 56,171 bytes under brotli&lt;/li&gt;
&lt;li&gt;/assets/rolldown-runtime-QTnfLwEv.js - 694 bytes raw, 403 bytes under brotli&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is 873,818 bytes of JavaScript raw and 263,818 bytes under brotli, plus 242,648 bytes of stylesheet. All five come back with cf-cache-status: HIT, so the edge serves them rather than the origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two fleets do not weigh the same
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;URLs&lt;/th&gt;
&lt;th&gt;bytes, uncompressed&lt;/th&gt;
&lt;th&gt;bytes, brotli&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App pages&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;19,132 to 19,576&lt;/td&gt;
&lt;td&gt;5,434 to 5,574&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guide pages&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;26,600 to 32,451&lt;/td&gt;
&lt;td&gt;6,768 to 7,613&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Studio pages&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;17,285 to 17,538&lt;/td&gt;
&lt;td&gt;5,022 to 5,126&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other routes&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;16,380 to 75,113&lt;/td&gt;
&lt;td&gt;4,689 to 18,221&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The five other routes are the home page, the vote board, the plans page, the terms page and the guides index, and the guides index is the one of them without a bundle.&lt;/p&gt;

&lt;p&gt;Across all 56 pages that is 1,386,556 bytes uncompressed, 369,515 bytes under gzip and 362,691 bytes under brotli. Brotli buys 6,824 bytes over gzip on our HTML, which is 1.8 percent - nowhere near the 70 percent it saves on the hashed JavaScript. The HTML is mostly one repeated shell, and compressors already do well on repetition.&lt;/p&gt;

&lt;p&gt;Read the other way round, the text the pages actually show is bigger in the static fleet. Stripping the tags out of the served HTML leaves 2,781 to 3,395 characters of readable text on a guide page and 1,778 to 2,088 on an app page. The app pages are the interactive ones, and the guide pages are the ones that say more before anything runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three smaller facts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;All 56 pages answer with transfer-encoding: chunked and none of them sends a content-length, so a client cannot know the size of the body before it finishes.&lt;/li&gt;
&lt;li&gt;Gzip and brotli are both honoured on every page. Zstandard is not: no page came back with a zstd Content-Encoding when asked.&lt;/li&gt;
&lt;li&gt;The two fallback files are served with a four-hour window rather than the pages' max-age=0: shell-fallback.css is 3,054 bytes and shell-fallback.js is 4,104 bytes, both public, max-age=14400, must-revalidate. The validators on this host are uneven: none of the 56 pages carries an ETag, all 5 hashed assets carry a strong one, the three text files a crawler reads - robots.txt, llms.txt and sitemap.xml - carry strong ETags too, and the two fallback files carry weak ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I did not do
&lt;/h2&gt;

&lt;p&gt;One build, one day, 8 November 2026, plain HTTPS from one network with no signed-in session. Each URL was fetched four times - once plain, then once each asking for gzip, brotli and zstd - so the byte totals are what the server sent in that window, not a fixed property of the build. Nothing here says what a page looks like once the app code runs, or how any crawler uses what it found; both need a browser and a reader, and this was neither. The zstd result is one request per URL, not a sweep of the encodings the origin supports. The guide pages are static documents and I did not open one in a browser to confirm what a reader sees.&lt;/p&gt;

&lt;p&gt;Try it yourself: &lt;a href="https://app.onepassapps.com/guides?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=guide-fleet-static&amp;amp;utm_content=guides-link" rel="noopener noreferrer"&gt;the app guides&lt;/a&gt;, and &lt;a href="https://app.onepassapps.com?utm_source=devto&amp;amp;utm_medium=post&amp;amp;utm_campaign=guide-fleet-static&amp;amp;utm_content=home-link" rel="noopener noreferrer"&gt;the site home&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Disclosure: I work on OnePassApps.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>performance</category>
      <category>seo</category>
      <category>html</category>
    </item>
    <item>
      <title>Stop Guessing: How to Build an LLM Recommender for Agentic AI into Any Web App</title>
      <dc:creator>Neweraofcoding</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:41:12 +0000</pubDate>
      <link>https://dev.to/sunny7899/stop-guessing-how-to-build-an-llm-recommender-for-agentic-ai-into-any-web-app-3j3f</link>
      <guid>https://dev.to/sunny7899/stop-guessing-how-to-build-an-llm-recommender-for-agentic-ai-into-any-web-app-3j3f</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffdq4l90xi8pbtswqnqfo.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffdq4l90xi8pbtswqnqfo.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When building agentic AI features into a web app, one of the most common pitfalls is hardcoding a single heavyweight LLM for every single operation.&lt;/p&gt;

&lt;p&gt;Using a frontier reasoning model just to classify user intent or format JSON wastes both latency and budget—while using a lightweight model for complex multi-step planning leads to brittle agent loops.&lt;/p&gt;

&lt;h3&gt;
  
  
  What This Guide Covers:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Task Classification&lt;/strong&gt;: Scoring incoming agent steps by reasoning depth, context size, and tool-use complexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoring Matrices&lt;/strong&gt;: Balancing latency budgets, per-token costs, and benchmark accuracy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommender Architecture&lt;/strong&gt;: Building a clean model-selection layer that plugs into any frontend or backend stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback &amp;amp; Telemetry Loops&lt;/strong&gt;: Handling rate limits gracefully and adjusting recommendations using real-world execution metrics&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Read the complete implementation guide on &lt;a href="https://buildwithsunny.com/posts/stop-guessing-how-to-build-an-llm-recommender-for-agentic-ai-into-any-web-app/" rel="noopener noreferrer"&gt;SunnyWriteUps&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>architecture</category>
    </item>
    <item>
      <title>0x Loom is out: a full dev environment in one browser tab</title>
      <dc:creator>Vasuki iTech International</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:38:06 +0000</pubDate>
      <link>https://dev.to/adamya_khairwal_9d2286eac/0x-loom-is-out-a-full-dev-environment-in-one-browser-tab-39j2</link>
      <guid>https://dev.to/adamya_khairwal_9d2286eac/0x-loom-is-out-a-full-dev-environment-in-one-browser-tab-39j2</guid>
      <description>&lt;h1&gt;
  
  
  The 0x Gazette: Release Edition
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;An editor, an AI pair, GitHub, notebooks and an extension store, with nothing to install and no project to set up before you write the first line.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Try it now: &lt;strong&gt;&lt;a href="https://0x.vasuki.cloud" rel="noopener noreferrer"&gt;0x.vasuki.cloud&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Most developer tools ask for an afternoon before they give you anything back. &lt;strong&gt;0x Loom&lt;/strong&gt; asks for a tab.&lt;/p&gt;

&lt;p&gt;Open it, drag a folder onto the page, and you are editing with syntax highlighting, auto-closing tags and autocomplete. Your files stay in your browser, and a reload brings back your tabs, your unsaved edits and your open folders.&lt;/p&gt;

&lt;p&gt;The goal of this first public release is simple: make the loop from idea to running code short enough that it works on a laptop, a borrowed machine or a phone. Python and Node run on a backend, HTML and SVG preview live beside the code, and a terminal handles &lt;code&gt;npm&lt;/code&gt; when a project needs packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  By the numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;project templates: Node.js, React (Vite) and Angular&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;15&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;extensions pre-installed on first run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;11&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;colour themes in the marketplace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;600&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;files loaded when you clone a repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;installs required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  An assistant that edits, not just chats
&lt;/h2&gt;

&lt;p&gt;The built-in Gemini assistant reads your open file, your selection and anything you mention with &lt;code&gt;@path/to/file&lt;/code&gt;. When it wants to change something, it doesn't paste a wall of code. It proposes a precise search-and-replace block, and you press &lt;strong&gt;Accept&lt;/strong&gt; or &lt;strong&gt;Reject&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If the text it wants to replace is missing or ambiguous, the edit is refused, so nothing changes by accident.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every suggestion arrives as a diff you can accept or reject. Nothing is applied behind your back.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Several API keys can sit behind the assistant. When one hits a rate limit, Loom moves to the next. An &lt;strong&gt;AI Billing&lt;/strong&gt; screen shows requests and tokens per key, so you can see a quota running out before it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  From first commit to a live site
&lt;/h2&gt;

&lt;p&gt;Connect GitHub once, then clone a repository or create a new one from a &lt;strong&gt;Node.js&lt;/strong&gt;, &lt;strong&gt;React (Vite)&lt;/strong&gt; or &lt;strong&gt;Angular&lt;/strong&gt; template. Loom scaffolds the files and pushes the first commit.&lt;/p&gt;

&lt;p&gt;Changes are listed file by file before you push, and build output and &lt;code&gt;node_modules&lt;/code&gt; are never sent.&lt;/p&gt;

&lt;p&gt;When you are ready to share, one button publishes to &lt;strong&gt;GitHub Pages&lt;/strong&gt;. For projects with a build script, Loom adds the deploy workflow, watches the build and hands you the live URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notebooks and a vault
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Shell&lt;/strong&gt; opens a Jupyter-style notebook with Python and Markdown cells. It reads and writes standard &lt;code&gt;.ipynb&lt;/code&gt; files, shares variables between cells, and saves to your browser, to disk or to a private GitHub repo.&lt;/p&gt;

&lt;p&gt;Secrets live in your &lt;strong&gt;Tijori&lt;/strong&gt; vault and can be dropped into a file at the cursor, so keys stay out of your source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it yours
&lt;/h2&gt;

&lt;p&gt;Extensions are plain JavaScript with a documented API for commands, panels, status items, key handlers and the AI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Loom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello&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;Say hello&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Loom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hi from &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;Extension Studio&lt;/strong&gt; gives you a code editor, a live README preview, version history and a safety scan that flags vault, network and file access before anyone installs your work. Publishing is tied to a key only you hold, and anyone else can fork instead of overwrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also in this edition
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Command palette on &lt;code&gt;Ctrl/⌘ K&lt;/code&gt; with text, JSON, Base64 and npm tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Map&lt;/strong&gt;: every function and variable, with all references across files&lt;/li&gt;
&lt;li&gt;Light and dark themes, built for phones as well as desktops&lt;/li&gt;
&lt;li&gt;Docs for Vasuki Traffic and Tijori in one place&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Open a tab. Start building.
&lt;/h2&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://0x.vasuki.cloud" rel="noopener noreferrer"&gt;Launch 0x Loom at 0x.vasuki.cloud&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd love feedback: what broke, what's missing, and which extension you'd build first. Drop it in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Struggling to Understand Someone Else's Code? Try an AI Code Explainer</title>
      <dc:creator>Monika Pareek</dc:creator>
      <pubDate>Sun, 08 Nov 2026 05:37:20 +0000</pubDate>
      <link>https://dev.to/monika_pareek_12444df9f98/struggling-to-understand-someone-elses-code-try-an-ai-code-explainer-2k6f</link>
      <guid>https://dev.to/monika_pareek_12444df9f98/struggling-to-understand-someone-elses-code-try-an-ai-code-explainer-2k6f</guid>
      <description>&lt;h1&gt;
  
  
  Struggling to Understand Someone Else's Code? Try an AI Code Explainer
&lt;/h1&gt;

&lt;p&gt;Every developer has faced this situation: you open a code file, see dozens of unfamiliar lines, and spend time trying to understand what everything does.&lt;/p&gt;

&lt;p&gt;This can be especially challenging for beginners, students, and developers working with unfamiliar codebases.&lt;/p&gt;

&lt;p&gt;AI can make this process easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing XoneAI Code Explainer
&lt;/h2&gt;

&lt;p&gt;I built a free AI-powered Code Explainer to help developers understand code more easily.&lt;/p&gt;

&lt;p&gt;With this tool, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get explanations of code.&lt;/li&gt;
&lt;li&gt;Understand unfamiliar programming logic.&lt;/li&gt;
&lt;li&gt;Learn how different parts of code work.&lt;/li&gt;
&lt;li&gt;Save time when exploring code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  No Signup Required
&lt;/h2&gt;

&lt;p&gt;You don't need to create an account or log in to try it.&lt;/p&gt;

&lt;p&gt;Try it here: &lt;a href="https://www.xoneai.in/tools/code-explainer" rel="noopener noreferrer"&gt;https://www.xoneai.in/tools/code-explainer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm building XoneAI to make useful AI tools accessible to developers and students.&lt;/p&gt;

&lt;p&gt;I'd love to hear your feedback!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What feature would make a code explainer more useful for your daily development work?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
