<?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: David Friedman</title>
    <description>The latest articles on DEV Community by David Friedman (@david_friedman_c2808375c1).</description>
    <link>https://dev.to/david_friedman_c2808375c1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3930934%2Fd66cf272-d519-4293-985b-4db1d3e05877.jpg</url>
      <title>DEV Community: David Friedman</title>
      <link>https://dev.to/david_friedman_c2808375c1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/david_friedman_c2808375c1"/>
    <language>en</language>
    <item>
      <title>Handling WhatsApp Cloud API webhooks without losing your mind: echoes, retries, and the five ways a message lies to you</title>
      <dc:creator>David Friedman</dc:creator>
      <pubDate>Sun, 30 Aug 2026 17:45:02 +0000</pubDate>
      <link>https://dev.to/david_friedman_c2808375c1/handling-whatsapp-cloud-api-webhooks-without-losing-your-mind-echoes-retries-and-the-five-ways-a-4c34</link>
      <guid>https://dev.to/david_friedman_c2808375c1/handling-whatsapp-cloud-api-webhooks-without-losing-your-mind-echoes-retries-and-the-five-ways-a-4c34</guid>
      <description>&lt;h1&gt;
  
  
  Handling WhatsApp Cloud API webhooks without losing your mind: echoes, retries, and the five ways a message lies to you
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;The &lt;a href="https://dev.to/david_friedman_c2808375c1/how-an-ai-receptionist-actually-books-an-appointment-over-whatsapp-calendar-sync-freebusy-and-350d"&gt;previous post&lt;/a&gt; covered how my AI receptionist books appointments. This one is about the part of the system nobody photographs: the webhook, where Meta tells you what happened. Slowly. Twice. Occasionally in the wrong order.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The promise vs the delivery
&lt;/h2&gt;

&lt;p&gt;The tutorial version of a WhatsApp integration: Meta POSTs you a message, you parse &lt;code&gt;messages[0].text.body&lt;/code&gt;, you reply. Ship it Friday.&lt;/p&gt;

&lt;p&gt;The production version is different. Your endpoint receives a payload with no message in it. Or the same message five times. Or a message &lt;em&gt;you yourself sent&lt;/em&gt;. Or a status update for a message you never saw, from a sender whose WhatsApp ID has silently changed case. Each of these is a different 2am incident, and all five show up within your first month of real volume.&lt;/p&gt;

&lt;p&gt;I learned each one the expensive way so you can learn it the cheap way. Here they are, ordered by blast radius.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Retries: the same webhook, five times
&lt;/h2&gt;

&lt;p&gt;Meta's webhook delivery is at-least-once. If your endpoint doesn't answer 200 within a few seconds - cold start, a slow downstream call, a deploy, anything - Meta queues a redelivery. Network blip? Redelivery. Deploy restarting? Redelivery.&lt;/p&gt;

&lt;p&gt;Now remember what the payload usually is. It's a customer asking to book something.&lt;/p&gt;

&lt;p&gt;If you process it twice, your AI books Saturday twice, confirms twice, and the customer quietly concludes your receptionist is an over-caffeinated intern. So: dedupe on the platform message ID, with a TTL store - a Firestore doc with a TTL policy, a Redis &lt;code&gt;SET NX EX&lt;/code&gt;, whatever you have. Check it &lt;em&gt;before&lt;/em&gt; any business logic runs. First delivery is real, everything else is a log line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /webhook
  → verify signature (X-Hub-Signature-256, HMAC of raw body)
  → extract every entry[].changes[].value
  → for each message: if seen(m.id) → skip
  → mark seen(m.id) BEFORE processing   ← not after
  → process
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at where "mark seen" sits. Before processing. Not after.&lt;/p&gt;

&lt;p&gt;I put it after, once. Two redeliveries landed a few milliseconds apart, both passed the check, both wrote to the database, and I owned a very apologetic conversation about a double-booked Saturday. If the write is after the check, two concurrent deliveries can both pass. And Meta &lt;em&gt;does&lt;/em&gt; retry across regions concurrently.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Echoes: your own bot arrives through the front door
&lt;/h2&gt;

&lt;p&gt;This is the sneakiest of the five. When your bot sends a message, WhatsApp also delivers that message back to your webhook, as if it were inbound. Your own reply, arriving through the front door like a stranger.&lt;/p&gt;

&lt;p&gt;Naive code responds to it. And then you have a bot answering itself. I have watched two instances of my own bot exchange politeness spirals, only stopping when something hit a rate limit, while the customer watched the "typing..." indicator flicker like a haunted office.&lt;/p&gt;

&lt;p&gt;Filter on the message source. Only process entries whose &lt;code&gt;from&lt;/code&gt; is the customer's WhatsApp ID, and ignore anything arriving under the App ID your own sends go out through. Before dedupe, not after, because echoes also fan out on retries.&lt;/p&gt;

&lt;p&gt;One counterintuitive note: the echo stream is secretly useful. It's a built-in delivery receipt for your own sends. Just don't build state on it until the filter is bulletproof - a conversation history that contains your bot's own messages looks identical to "the customer said this," and every downstream feature breaks at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Statuses are not messages, except when they arrive for ghosts
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;statuses&lt;/code&gt; payload is delivery metadata: &lt;code&gt;sent&lt;/code&gt;, &lt;code&gt;delivered&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;. It references a message ID that should exist in your store.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Should.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;During a webhook outage, a manual re-subscribe, or a number migration, statuses arrive for messages your system never stored. That's a ghost reference. And if your code assumes the lookup succeeds - or worse, throws a 500 when it doesn't - you've just told Meta to retry. Forever. A retry loop over a dead reference.&lt;/p&gt;

&lt;p&gt;Accept the webhook with 200 and move on. A status for a ghost costs nothing to ignore. Crashing on it costs you the queue.&lt;/p&gt;

&lt;p&gt;And persist &lt;code&gt;read&lt;/code&gt; receipts properly, because they matter more than they look like they do. Suppressing a bot takeover when the human on your team is mid-conversation - that depends on knowing whose messages are being read, and when.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The 24-hour window, and the errors Meta doesn't advertise
&lt;/h2&gt;

&lt;p&gt;Outside 24 hours from a customer's last message, you can't send free-form text. You send an approved template. Fine, you think. Except the failure isn't clean. The API accepts your call, Meta queues the message, and &lt;em&gt;later&lt;/em&gt; a &lt;code&gt;failed&lt;/code&gt; status arrives with error &lt;code&gt;131047&lt;/code&gt; - re-engagement required.&lt;/p&gt;

&lt;p&gt;Which means: your booking confirmation queue reported success. The customer received nothing. The only trace is a delayed status webhook that your older code treated as noise. I know, because that was my older code.&lt;/p&gt;

&lt;p&gt;Parse &lt;code&gt;statuses[].errors[]&lt;/code&gt; from day one. &lt;code&gt;131047&lt;/code&gt; means send the template version instead - so actually send it, automatically. &lt;code&gt;131026&lt;/code&gt; (undeliverable) usually means a genuinely dead line: mark the contact, stop spending effort on it. Each code class needs its own handling, and the default handler should never be "ignore."&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Webhooks for conversations you're no longer in
&lt;/h2&gt;

&lt;p&gt;This last one is business logic wearing a technical costume.&lt;/p&gt;

&lt;p&gt;Human takes over a conversation in the shared inbox. Customer replies. Webhook fires. The bot answers too? Obviously not - there's a takeover flag. I use a 30 minute cooldown: long enough that the human clearly has the wheel, short enough that a forgotten conversation doesn't dead-end.&lt;/p&gt;

&lt;p&gt;But flags rot, and the dangerous window isn't the one you're thinking about. It's this: the AI decides to send at 10:00:00, and a human replies to the same thread at 10:00:04. Your pre-send check happened before their reply landed.&lt;/p&gt;

&lt;p&gt;So the check has to run immediately before every automated send, not just when the inbound message arrives. The webhook fires; the state of the conversation is the only truth. Same discipline as the dedupe - trust the flag, but read it fresh, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify token, signatures, and the boring gate
&lt;/h2&gt;

&lt;p&gt;Setup: Meta GETs you with &lt;code&gt;hub.mode=subscribe&lt;/code&gt; and a &lt;code&gt;hub.verify_token&lt;/code&gt;, you echo &lt;code&gt;hub.challenge&lt;/code&gt; if the token matches. Every POST after that carries &lt;code&gt;X-Hub-Signature-256&lt;/code&gt;, an HMAC-SHA256 of the raw body with your app secret.&lt;/p&gt;

&lt;p&gt;Two details that bite:&lt;/p&gt;

&lt;p&gt;Compute the HMAC on raw bytes. Before JSON parsing. Framework body parsers will happily consume the stream otherwise - both Express's &lt;code&gt;body-parser&lt;/code&gt; and Next.js route handlers need explicit raw-body capture. I learned this from a signature check that passed locally and failed in staging, which is exactly the kind of sentence you never want to have to say out loud.&lt;/p&gt;

&lt;p&gt;And compare timing-safe, not &lt;code&gt;===&lt;/code&gt;. If the signature fails, 401 immediately, and log the app ID - the most common cause is two Meta apps firing at the same endpoint, and they look identical once the raw body is gone.&lt;/p&gt;

&lt;p&gt;Last thing: run a subscription audit on a schedule. List what each Meta app is actually subscribed to (&lt;code&gt;messages&lt;/code&gt;, &lt;code&gt;message_template_status_update&lt;/code&gt;, whatever your code assumes) and diff it against what you handle. Subscriptions silently vanish when an app gets re-authed. The bug is invisible until the day a whole channel goes quiet, and then you'll spend hours checking every other layer first. I did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;

&lt;p&gt;Every defense above is the same idea wearing five hats. The webhook is a stream of &lt;em&gt;claims&lt;/em&gt;, not facts. Dedupe the claims. Filter the echoes out. Accept the unverifiable ones quietly. And before you act on any of them, re-read the world's actual state.&lt;/p&gt;

&lt;p&gt;Meta's delivery model is at-least-once, along every dimension - messages, statuses, retries, regions. The only safe architecture assumes every callback arrives zero, one, or several times, and that all three cases are normal.&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://conversify.app/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=founder-story" rel="noopener noreferrer"&gt;Conversify&lt;/a&gt;, an AI receptionist that lives in front of these webhooks full time - answering, booking and selling over WhatsApp, Instagram, Messenger, email and web chat for small service businesses. Self-serve, 14-day free trial, and it tells people it's AI.&lt;/p&gt;

&lt;p&gt;Next in the series, the calendar half of the story continued: availability search that survives multi-staff, blocked times, and the customer whose phone is in another timezone. Part one is &lt;a href="https://dev.to/david_friedman_c2808375c1/how-an-ai-receptionist-actually-books-an-appointment-over-whatsapp-calendar-sync-freebusy-and-350d"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>whatsapp</category>
      <category>webhooks</category>
      <category>backend</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How an AI receptionist actually books an appointment over WhatsApp (calendar sync, freeBusy, and why "no" should never be the final answer)</title>
      <dc:creator>David Friedman</dc:creator>
      <pubDate>Sun, 30 Aug 2026 17:42:39 +0000</pubDate>
      <link>https://dev.to/david_friedman_c2808375c1/how-an-ai-receptionist-actually-books-an-appointment-over-whatsapp-calendar-sync-freebusy-and-350d</link>
      <guid>https://dev.to/david_friedman_c2808375c1/how-an-ai-receptionist-actually-books-an-appointment-over-whatsapp-calendar-sync-freebusy-and-350d</guid>
      <description>&lt;h1&gt;
  
  
  How an AI receptionist actually books an appointment over WhatsApp (calendar sync, freeBusy, and why "no" should never be the final answer)
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;A technical walkthrough of the booking pipeline. Plus a detour through behavioral science, because the hard part of scheduling was never the calendar.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Rory Sutherland has a line that runs my product
&lt;/h2&gt;

&lt;p&gt;The Ogilvy vice chairman has spent twenty years making the same argument: most product problems aren't engineering problems. They're psycho-logic problems. People don't experience your infrastructure. They experience the interaction.&lt;/p&gt;

&lt;p&gt;His favorite example is the train operator that put a schedule on the platform. Waits didn't get shorter. They just stopped feeling infinite. The problem was never the waiting. It was the not knowing.&lt;/p&gt;

&lt;p&gt;Booking a haircut is exactly this. The customer's actual complaint isn't "you lack scheduling software." It's the interaction cost. Interrupt you, or wait, or call and hope. Every option is socially expensive for a haircut.&lt;/p&gt;

&lt;p&gt;And on the other side of the counter: the owner's complaint is that 11pm customers message, hear nothing, and quietly book the salon down the street instead.&lt;/p&gt;

&lt;p&gt;Both sides want the same thing. The transaction, minus the negotiation. That reframe &lt;em&gt;is&lt;/em&gt; the product. Everything below is engineering in service of killing the negotiation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "just connect a calendar" turns out to mean
&lt;/h2&gt;

&lt;p&gt;Here's the message that ends in a real booking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Hi! Do you have anything Saturday afternoon for a balayage?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Five lines. To answer honestly - not "probably!", not "let me check and get back to you", but a slot that will survive contact with reality - the AI needs all of this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Who can perform it.&lt;/strong&gt; A multi-staff salon isn't one calendar. It's N calendars, and not everyone does balayage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's free.&lt;/strong&gt; Existing bookings plus every external event on each staff member's personal Google Calendar. The dentist. The school run. Everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the business allows.&lt;/strong&gt; Opening hours, blocked times, per-staff daily booking caps, and whether the slot is even long enough for the service plus buffer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where the customer is.&lt;/strong&gt; A Madrid salon with a Miami WhatsApp number needs two different "3pm"s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An answer in seconds.&lt;/strong&gt; Because response time is the signal. A reply that lands while the customer is still holding the phone says "this business is alive." A perfect answer two hours later says "this will be a project."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last point is why the AI lives in the message path, wired to the calendar, and not behind a "click here to book" link. The moment the customer has to open a second app, you've rebuilt the negotiation you were hired to remove.&lt;/p&gt;

&lt;p&gt;This is the core promise of &lt;a href="https://conversify.app/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=founder-story" rel="noopener noreferrer"&gt;Conversify&lt;/a&gt;: calendar-wired answers in seconds, not "we'll get back to you."&lt;/p&gt;

&lt;h2&gt;
  
  
  The availability pipeline
&lt;/h2&gt;

&lt;p&gt;Every booking-capable message runs through roughly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;message → intent + service + preference(s)
        → candidate staff (skills, active status)
        → for each candidate:
             working hours ∩ unblocked time
             − calendar events (local + Google freeBusy)
             − per-day booking cap for that staff member
             − service duration + buffer
        → ranked slots (earliest match wins, preferences win)
        → offer 1-3 concrete options → customer picks → book
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part developers underestimate is always freeBusy. Here's the mistake I made first: caching calendars. Feels sensible. It isn't. Staff edit their calendars from their phones in the checkout line, and your cache is one edit behind, always. The fix is dumb - query &lt;code&gt;freeBusy&lt;/code&gt; live for every candidate slot - and it wiped out the entire class of "I double-booked a haircut against my dentist appointment" complaints. Cost is an API call per candidate. Fine. Buy the API calls.&lt;/p&gt;

&lt;p&gt;Blocked times are the quiet hero of the whole design. An owner drops "closed 2-3pm daily" as a blocked range and it applies everywhere - the AI conversation, the public booking page, the manual booking screen. One source of truth. I cannot stress this enough: if the bot, the booking page, and the staff each have their own idea of availability, you will ship a bot that cheerfully sells appointments during the owner's kid's birthday party. Ask me how I know.&lt;/p&gt;

&lt;p&gt;Timezones you solve boringly and correctly. Store absolute times. Display the customer's local zone. "Saturday at 4pm" must read back to the customer as &lt;em&gt;their&lt;/em&gt; 4pm. Sounds obvious. The first bug report where it isn't will burn a day of your life.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirmation, reminders, and the "no" that becomes a "yes"
&lt;/h2&gt;

&lt;p&gt;Booking isn't the finish line. WhatsApp bookings have a specific failure mode: the customer says yes in chat, then never shows, because the appointment lives in a chat bubble that has scrolled away under a week of family group messages.&lt;/p&gt;

&lt;p&gt;So the loop closes in three beats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instant confirmation.&lt;/strong&gt; A proper WhatsApp template message - the utility-category kind, pre-approved so it can arrive outside the 24-hour service window - with date, time, staff name, and location.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A confirmation request&lt;/strong&gt; the customer can literally answer "YES" or "NO". This isn't bureaucracy. It's a super-stimulus of reliability. The business is visibly keeping its act together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A reminder before the appointment.&lt;/strong&gt; And if it's cancelled, the slot returns to the pool immediately, and waitlisted requests for that exact window get first refusal.&lt;/p&gt;

&lt;p&gt;That last part is where Sutherland earns his keep. In psycho-logic terms, a "no" is not an outcome. It's a failure of imagination.&lt;/p&gt;

&lt;p&gt;The naive system hits Saturday-is-full and says "sorry, we're fully booked." Conversation dies. The customer's mood flips from "I want this salon" to "I want this to be over," and they call whoever answers next.&lt;/p&gt;

&lt;p&gt;The right system treats full-ness as a reframe. Offer the nearest alternative. Offer the same stylist on a different day. Capture the request against the waitlist for cancelled slots. The customer now experiences a business &lt;em&gt;fighting to fit them in&lt;/em&gt;, which is the entire signal an appointment was supposed to deliver in the first place. Same calendar. Same slots. Different problem. That is the trick Sutherland keeps pointing at, and most scheduling software has never heard of it.&lt;/p&gt;

&lt;p&gt;Same discipline for escalation: if a human has stepped into the thread, the AI stands down before booking anything. It re-checks with every reply, mid-conversation. An AI that books over a human's active conversation signals chaos, and chaos is the one signal you can't afford.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack this runs on
&lt;/h2&gt;

&lt;p&gt;Next.js and Firebase App Hosting. The WhatsApp Business API for templates and webhooks. Genkit on Google's Gemini for the understanding layer. Firestore for state. Google Calendar APIs for the truth about time.&lt;/p&gt;

&lt;p&gt;One rule above all of it: the calendar is the source of truth, and the AI is a very polite client of it. It never invents availability. It only resolves it.&lt;/p&gt;

&lt;p&gt;That sentence is most of the product, honestly. A receptionist that hallucinates Saturday openings is worse than no receptionist, because it converts a missed message into a broken promise with a witness.&lt;/p&gt;

&lt;h2&gt;
  
  
  If this is your problem too
&lt;/h2&gt;

&lt;p&gt;I build &lt;a href="https://conversify.app/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=founder-story" rel="noopener noreferrer"&gt;Conversify&lt;/a&gt; - an AI receptionist that answers, books and sells over WhatsApp, Instagram, Messenger, email and web chat, for people who have a business to run more than an inbox to administer. Self-serve. Minutes to set up, not months. 14-day free trial. And it always tells the person it's AI.&lt;/p&gt;

&lt;p&gt;Next post in this series, if it survives the fix list: handling WhatsApp Cloud API webhooks without deduplicating yourself into despair.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>firebase</category>
      <category>googlecalendar</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Event-Driven Architecture with WebSockets</title>
      <dc:creator>David Friedman</dc:creator>
      <pubDate>Thu, 14 May 2026 15:57:36 +0000</pubDate>
      <link>https://dev.to/david_friedman_c2808375c1/event-driven-architecture-with-websockets-18h</link>
      <guid>https://dev.to/david_friedman_c2808375c1/event-driven-architecture-with-websockets-18h</guid>
      <description>&lt;h1&gt;
  
  
  Event-Driven Architecture with WebSockets
&lt;/h1&gt;

&lt;p&gt;Real-time communication patterns for modern web apps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://appbrewers.com" rel="noopener noreferrer"&gt;AppBrewers&lt;/a&gt; — real-time development Malta.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Database Sharding Strategies for SaaS</title>
      <dc:creator>David Friedman</dc:creator>
      <pubDate>Thu, 14 May 2026 15:56:43 +0000</pubDate>
      <link>https://dev.to/david_friedman_c2808375c1/database-sharding-strategies-for-saas-3j57</link>
      <guid>https://dev.to/david_friedman_c2808375c1/database-sharding-strategies-for-saas-3j57</guid>
      <description>&lt;h1&gt;
  
  
  Database Sharding Strategies for SaaS
&lt;/h1&gt;

&lt;p&gt;Horizontal scaling patterns for multi-tenant applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://appbrewers.com" rel="noopener noreferrer"&gt;AppBrewers&lt;/a&gt; — SaaS backend specialists Malta.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Edge Computing with Cloudflare Workers</title>
      <dc:creator>David Friedman</dc:creator>
      <pubDate>Thu, 14 May 2026 15:41:42 +0000</pubDate>
      <link>https://dev.to/david_friedman_c2808375c1/edge-computing-with-cloudflare-workers-436j</link>
      <guid>https://dev.to/david_friedman_c2808375c1/edge-computing-with-cloudflare-workers-436j</guid>
      <description>&lt;h1&gt;
  
  
  Edge Computing with Cloudflare Workers
&lt;/h1&gt;

&lt;p&gt;Run code at the edge for sub-50ms response times.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://appbrewers.com" rel="noopener noreferrer"&gt;AppBrewers&lt;/a&gt; — edge computing specialists Malta.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>AI Agent Architecture Patterns 2026</title>
      <dc:creator>David Friedman</dc:creator>
      <pubDate>Thu, 14 May 2026 15:41:25 +0000</pubDate>
      <link>https://dev.to/david_friedman_c2808375c1/ai-agent-architecture-patterns-2026-5h4p</link>
      <guid>https://dev.to/david_friedman_c2808375c1/ai-agent-architecture-patterns-2026-5h4p</guid>
      <description>&lt;h1&gt;
  
  
  AI Agent Architecture Patterns 2026
&lt;/h1&gt;

&lt;p&gt;ReAct, Plan-and-Execute, and Multi-Agent patterns explained.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://appbrewers.com" rel="noopener noreferrer"&gt;AppBrewers&lt;/a&gt; — AI development agency Malta.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>AI Receptionist vs Human Receptionist</title>
      <dc:creator>David Friedman</dc:creator>
      <pubDate>Thu, 14 May 2026 15:02:39 +0000</pubDate>
      <link>https://dev.to/david_friedman_c2808375c1/ai-receptionist-vs-human-receptionist-85n</link>
      <guid>https://dev.to/david_friedman_c2808375c1/ai-receptionist-vs-human-receptionist-85n</guid>
      <description>&lt;h1&gt;
  
  
  AI Receptionist vs Human Receptionist\n\nComparing costs, availability, and customer experience.\n\n&lt;a href="https://appbrewers.com" rel="noopener noreferrer"&gt;AppBrewers&lt;/a&gt; builds AI receptionists for businesses.
&lt;/h1&gt;

</description>
    </item>
    <item>
      <title>Mobile App Development Trends 2026</title>
      <dc:creator>David Friedman</dc:creator>
      <pubDate>Thu, 14 May 2026 14:54:22 +0000</pubDate>
      <link>https://dev.to/david_friedman_c2808375c1/mobile-app-development-trends-2026-15m1</link>
      <guid>https://dev.to/david_friedman_c2808375c1/mobile-app-development-trends-2026-15m1</guid>
      <description>&lt;h1&gt;
  
  
  Mobile App Development Trends 2026
&lt;/h1&gt;

&lt;p&gt;Key trends shaping the mobile industry this year.&lt;/p&gt;

&lt;p&gt;Learn more at &lt;a href="https://appbrewers.com" rel="noopener noreferrer"&gt;AppBrewers&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Web Development Best Practices 2026</title>
      <dc:creator>David Friedman</dc:creator>
      <pubDate>Thu, 14 May 2026 14:54:02 +0000</pubDate>
      <link>https://dev.to/david_friedman_c2808375c1/web-development-best-practices-2026-4h5l</link>
      <guid>https://dev.to/david_friedman_c2808375c1/web-development-best-practices-2026-4h5l</guid>
      <description>&lt;h1&gt;
  
  
  Web Development Best Practices 2026
&lt;/h1&gt;

&lt;p&gt;Tips for building modern web apps.&lt;/p&gt;

&lt;p&gt;Read more at &lt;a href="https://appbrewers.com" rel="noopener noreferrer"&gt;AppBrewers&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Progressive Web Apps vs Native Apps: Which Should You Build in 2026?</title>
      <dc:creator>David Friedman</dc:creator>
      <pubDate>Thu, 14 May 2026 14:42:18 +0000</pubDate>
      <link>https://dev.to/david_friedman_c2808375c1/progressive-web-apps-vs-native-apps-which-should-you-build-in-2026-4o0o</link>
      <guid>https://dev.to/david_friedman_c2808375c1/progressive-web-apps-vs-native-apps-which-should-you-build-in-2026-4o0o</guid>
      <description>&lt;p&gt;&lt;em&gt;PWAs have come a long way. Here is when they beat native apps — and when they do not.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By &lt;a href="https://appbrewers.com" rel="noopener noreferrer"&gt;David Friedman&lt;/a&gt;, Founder of &lt;a href="https://appbrewers.com" rel="noopener noreferrer"&gt;AppBrewers&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Progressive Web Apps (PWAs) promise native-like experience without app store submission. But they are not a universal replacement. We have built both. Here is the honest comparison.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is a PWA?
&lt;/h2&gt;

&lt;p&gt;A PWA is a web app that uses service workers, manifests, and modern APIs to provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Offline functionality&lt;/li&gt;
&lt;li&gt;Push notifications&lt;/li&gt;
&lt;li&gt;Home screen installation&lt;/li&gt;
&lt;li&gt;Background sync&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  PWA Advantages
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Advantage&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No app store gatekeeping&lt;/td&gt;
&lt;td&gt;Update instantly, no review process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lower development cost&lt;/td&gt;
&lt;td&gt;One codebase for all platforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO discoverable&lt;/td&gt;
&lt;td&gt;Indexed by Google, found organically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No installation friction&lt;/td&gt;
&lt;td&gt;Visit URL, add to home screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smaller download size&lt;/td&gt;
&lt;td&gt;Kilobytes vs megabytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Native App Advantages
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Advantage&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full hardware access&lt;/td&gt;
&lt;td&gt;Bluetooth, NFC, ARKit, sensors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App store distribution&lt;/td&gt;
&lt;td&gt;Trust signal, organic discovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background processing&lt;/td&gt;
&lt;td&gt;Location tracking, audio playback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Better performance&lt;/td&gt;
&lt;td&gt;Compiled code, not JavaScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push notification reliability&lt;/td&gt;
&lt;td&gt;iOS APNS is more reliable than web push&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Decision Matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Choose PWA&lt;/th&gt;
&lt;th&gt;Choose Native&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Offline-first&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push notifications&lt;/td&gt;
&lt;td&gt;Yes (Android)&lt;/td&gt;
&lt;td&gt;Yes (iOS + Android)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camera/Bluetooth/AR&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Budget under 10k&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Maybe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App store presence&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rapid iteration&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Our Recommendation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Build a PWA if:&lt;/strong&gt; Your app is content-focused, has simple interactions, or needs fast deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build native if:&lt;/strong&gt; You need hardware access, complex animations, or app store distribution is critical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build both if:&lt;/strong&gt; Budget allows. PWA for reach, native for power users.&lt;/p&gt;




&lt;h2&gt;
  
  
  PWA Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Next.js 15 / Nuxt 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service Worker&lt;/td&gt;
&lt;td&gt;Workbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;Zustand / Pinia&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Firebase / Supabase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting&lt;/td&gt;
&lt;td&gt;Vercel / Firebase Hosting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Need a PWA or Native App Built?
&lt;/h2&gt;

&lt;p&gt;We build PWAs and native apps for startups. React Native, Flutter, or Next.js PWA. 6-10 weeks. Fixed pricing. Malta-based.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://appbrewers.com/get-a-quote" rel="noopener noreferrer"&gt;appbrewers.com/get-a-quote&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="mailto:hello@appbrewers.com"&gt;hello@appbrewers.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://appbrewers.com/blog/pwa-vs-native-apps-2026" rel="noopener noreferrer"&gt;AppBrewers Blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>native</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>No-Code vs Custom Development: When to Switch</title>
      <dc:creator>David Friedman</dc:creator>
      <pubDate>Thu, 14 May 2026 14:37:07 +0000</pubDate>
      <link>https://dev.to/david_friedman_c2808375c1/no-code-vs-custom-development-when-to-switch-32n</link>
      <guid>https://dev.to/david_friedman_c2808375c1/no-code-vs-custom-development-when-to-switch-32n</guid>
      <description>&lt;p&gt;&lt;em&gt;No-code tools get you to MVP fast. But there is a point where they become a liability. Here is how to know when to switch.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By &lt;a href="https://appbrewers.com" rel="noopener noreferrer"&gt;David Friedman&lt;/a&gt;, Founder of &lt;a href="https://appbrewers.com" rel="noopener noreferrer"&gt;AppBrewers&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;We have rescued 8 products from no-code platforms that could not scale. We have also advised founders to stay on no-code longer than they planned. Here is the framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  The No-Code Sweet Spot
&lt;/h2&gt;

&lt;p&gt;No-code is perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validating ideas with real users&lt;/li&gt;
&lt;li&gt;Internal tools and dashboards&lt;/li&gt;
&lt;li&gt;Simple marketplaces and directories&lt;/li&gt;
&lt;li&gt;Landing pages and marketing sites&lt;/li&gt;
&lt;li&gt;MVPs with standard features&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When No-Code Breaks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Page load &amp;gt;3 seconds&lt;/td&gt;
&lt;td&gt;Custom backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom logic&lt;/td&gt;
&lt;td&gt;Workarounds everywhere&lt;/td&gt;
&lt;td&gt;Custom code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale&lt;/td&gt;
&lt;td&gt;100+ concurrent users&lt;/td&gt;
&lt;td&gt;Optimized architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;Missing API connectors&lt;/td&gt;
&lt;td&gt;Custom integrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;500+ Euro/month for simple app&lt;/td&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Platform Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Switch Point&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bubble&lt;/td&gt;
&lt;td&gt;Complex web apps&lt;/td&gt;
&lt;td&gt;1,000+ users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webflow&lt;/td&gt;
&lt;td&gt;Marketing sites&lt;/td&gt;
&lt;td&gt;Ecommerce at scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Airtable&lt;/td&gt;
&lt;td&gt;Internal tools&lt;/td&gt;
&lt;td&gt;External users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zapier&lt;/td&gt;
&lt;td&gt;Automation&lt;/td&gt;
&lt;td&gt;100+ zaps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adalo&lt;/td&gt;
&lt;td&gt;Simple mobile apps&lt;/td&gt;
&lt;td&gt;Custom features&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Hybrid Approach
&lt;/h2&gt;

&lt;p&gt;Many successful startups use no-code for the frontend and custom code for the backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend: Webflow&lt;/li&gt;
&lt;li&gt;Backend: Firebase Functions + Firestore&lt;/li&gt;
&lt;li&gt;Auth: Firebase Auth&lt;/li&gt;
&lt;li&gt;Payments: Stripe&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you speed and scalability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration Strategy
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Timeline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Audit no-code limitations&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Design custom architecture&lt;/td&gt;
&lt;td&gt;2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Build core features&lt;/td&gt;
&lt;td&gt;4-8 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Migrate data&lt;/td&gt;
&lt;td&gt;1-2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Parallel run + switch&lt;/td&gt;
&lt;td&gt;1 week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Decommission no-code&lt;/td&gt;
&lt;td&gt;Immediate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Cost of Switching
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Custom development&lt;/td&gt;
&lt;td&gt;15,000-50,000 Euro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data migration&lt;/td&gt;
&lt;td&gt;2,000-5,000 Euro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downtime risk&lt;/td&gt;
&lt;td&gt;Revenue dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team retraining&lt;/td&gt;
&lt;td&gt;1-2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Need Help Migrating from No-Code?
&lt;/h2&gt;

&lt;p&gt;We migrate products from Bubble, Webflow, and Adalo to custom Next.js and React Native. No data loss. Zero downtime. 6-10 weeks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://appbrewers.com/get-a-quote" rel="noopener noreferrer"&gt;appbrewers.com/get-a-quote&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="mailto:hello@appbrewers.com"&gt;hello@appbrewers.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://appbrewers.com/blog/no-code-vs-custom-development" rel="noopener noreferrer"&gt;AppBrewers Blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nocode</category>
      <category>webdev</category>
      <category>startup</category>
      <category>mvp</category>
    </item>
    <item>
      <title>Web App Security Checklist 2026: Protect Your Startup from Day One</title>
      <dc:creator>David Friedman</dc:creator>
      <pubDate>Thu, 14 May 2026 14:35:56 +0000</pubDate>
      <link>https://dev.to/david_friedman_c2808375c1/web-app-security-checklist-2026-protect-your-startup-from-day-one-55m4</link>
      <guid>https://dev.to/david_friedman_c2808375c1/web-app-security-checklist-2026-protect-your-startup-from-day-one-55m4</guid>
      <description>&lt;p&gt;&lt;em&gt;Security is not a feature you add later. It is a foundation you build from day one. Here is our production checklist.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By &lt;a href="https://appbrewers.com" rel="noopener noreferrer"&gt;David Friedman&lt;/a&gt;, Founder of &lt;a href="https://appbrewers.com" rel="noopener noreferrer"&gt;AppBrewers&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;We have audited 20+ web apps for security. The same vulnerabilities appear repeatedly. Here is the checklist we use for every project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Authentication
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Use OAuth 2.0 / OpenID Connect (Firebase Auth, Clerk, Auth0)&lt;/li&gt;
&lt;li&gt;[ ] Enforce strong passwords (8+ chars, complexity requirements)&lt;/li&gt;
&lt;li&gt;[ ] Implement brute-force protection (rate limiting)&lt;/li&gt;
&lt;li&gt;[ ] Use secure session tokens (HTTPOnly, SameSite, Secure flags)&lt;/li&gt;
&lt;li&gt;[ ] Add multi-factor authentication for admin roles&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Authorization
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Verify permissions on every API endpoint (server-side)&lt;/li&gt;
&lt;li&gt;[ ] Use Role-Based Access Control (RBAC) or Attribute-Based Access Control (ABAC)&lt;/li&gt;
&lt;li&gt;[ ] Never trust client-side permission checks&lt;/li&gt;
&lt;li&gt;[ ] Validate resource ownership (user A cannot access user B's data)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Data Protection
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Encrypt data at rest (AES-256)&lt;/li&gt;
&lt;li&gt;[ ] Encrypt data in transit (TLS 1.3 minimum)&lt;/li&gt;
&lt;li&gt;[ ] Hash passwords with bcrypt / Argon2 (never MD5 or SHA1)&lt;/li&gt;
&lt;li&gt;[ ] Mask sensitive data in logs&lt;/li&gt;
&lt;li&gt;[ ] Implement GDPR-compliant data deletion&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Input Validation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Validate all inputs server-side (never trust client)&lt;/li&gt;
&lt;li&gt;[ ] Sanitize HTML to prevent XSS&lt;/li&gt;
&lt;li&gt;[ ] Use parameterized queries (prevent SQL injection)&lt;/li&gt;
&lt;li&gt;[ ] Validate file uploads (type, size, content scan)&lt;/li&gt;
&lt;li&gt;[ ] Set strict Content Security Policy headers&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Infrastructure
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Use HTTPS everywhere (HSTS enabled)&lt;/li&gt;
&lt;li&gt;[ ] Keep dependencies updated (automated scanning)&lt;/li&gt;
&lt;li&gt;[ ] Implement DDoS protection (Cloudflare, AWS Shield)&lt;/li&gt;
&lt;li&gt;[ ] Enable Web Application Firewall (WAF)&lt;/li&gt;
&lt;li&gt;[ ] Regular security audits and penetration testing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Monitoring
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Log all authentication attempts&lt;/li&gt;
&lt;li&gt;[ ] Alert on suspicious activity (impossible travel, brute force)&lt;/li&gt;
&lt;li&gt;[ ] Monitor for dependency vulnerabilities (Snyk, Dependabot)&lt;/li&gt;
&lt;li&gt;[ ] Incident response plan documented&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common Vulnerabilities We Fix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;XSS&lt;/td&gt;
&lt;td&gt;Session hijacking&lt;/td&gt;
&lt;td&gt;CSP + input sanitization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSRF&lt;/td&gt;
&lt;td&gt;Unauthorized actions&lt;/td&gt;
&lt;td&gt;SameSite cookies + tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Injection&lt;/td&gt;
&lt;td&gt;Database access&lt;/td&gt;
&lt;td&gt;Parameterized queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDOR&lt;/td&gt;
&lt;td&gt;Data leakage&lt;/td&gt;
&lt;td&gt;Server-side authorization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Insecure Direct Object Reference&lt;/td&gt;
&lt;td&gt;Account takeover&lt;/td&gt;
&lt;td&gt;UUIDs + permission checks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Need a Security Audit?
&lt;/h2&gt;

&lt;p&gt;We build secure web apps from the ground up. Auth, encryption, compliance. Penetration testing available. Malta-based security team.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://appbrewers.com/get-a-quote" rel="noopener noreferrer"&gt;appbrewers.com/get-a-quote&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="mailto:hello@appbrewers.com"&gt;hello@appbrewers.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://appbrewers.com/blog/web-app-security-checklist-2026" rel="noopener noreferrer"&gt;AppBrewers Blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>startup</category>
      <category>bestpractices</category>
    </item>
  </channel>
</rss>
