<?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: Akin Ajobo</title>
    <description>The latest articles on DEV Community by Akin Ajobo (@mistakili).</description>
    <link>https://dev.to/mistakili</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%2F4007662%2Fbbdd2e8c-3b1b-49d4-81e9-65cbc1e1503e.jpg</url>
      <title>DEV Community: Akin Ajobo</title>
      <link>https://dev.to/mistakili</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mistakili"/>
    <language>en</language>
    <item>
      <title>The Production Bug No Capacitor Push Notification Tutorial Prepared Me For</title>
      <dc:creator>Akin Ajobo</dc:creator>
      <pubDate>Mon, 29 Jun 2026 08:39:58 +0000</pubDate>
      <link>https://dev.to/mistakili/the-production-bug-no-capacitor-push-notification-tutorial-prepared-me-for-1386</link>
      <guid>https://dev.to/mistakili/the-production-bug-no-capacitor-push-notification-tutorial-prepared-me-for-1386</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Everything worked perfectly... until I uploaded my app to TestFlight.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every push notification tutorial I read covered the same things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Register for push notifications.&lt;/li&gt;
&lt;li&gt;Request permission.&lt;/li&gt;
&lt;li&gt;Get the device token.&lt;/li&gt;
&lt;li&gt;Send your first notification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And to be fair, all of those tutorials worked.&lt;/p&gt;

&lt;p&gt;Until they didn't.&lt;/p&gt;

&lt;p&gt;The moment I uploaded my Capacitor app to TestFlight, the entire push notification flow quietly fell apart.&lt;/p&gt;

&lt;p&gt;No obvious errors.&lt;/p&gt;

&lt;p&gt;No compiler issues.&lt;/p&gt;

&lt;p&gt;No helpful stack traces.&lt;/p&gt;

&lt;p&gt;Just... silence.&lt;/p&gt;

&lt;p&gt;Over the next two days, I went down one rabbit hole after another before discovering that the problem wasn't a single bug—it was a collection of production-only assumptions that none of the tutorials mentioned.&lt;/p&gt;

&lt;p&gt;This article is the postmortem I wish I'd found before I started.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;The stack was pretty standard.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Capacitor&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Node.js backend&lt;/li&gt;
&lt;li&gt;APNs&lt;/li&gt;
&lt;li&gt;iOS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything behaved exactly as expected during development.&lt;/p&gt;

&lt;p&gt;Push permissions appeared.&lt;/p&gt;

&lt;p&gt;The device registered.&lt;/p&gt;

&lt;p&gt;Tokens looked correct.&lt;/p&gt;

&lt;p&gt;Notifications arrived instantly.&lt;/p&gt;

&lt;p&gt;I genuinely thought this part of the app was finished.&lt;/p&gt;

&lt;p&gt;Then I created a TestFlight build.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Moment Everything Broke
&lt;/h2&gt;

&lt;p&gt;I installed the TestFlight build.&lt;/p&gt;

&lt;p&gt;Opened the app.&lt;/p&gt;

&lt;p&gt;Tapped my "Send Test Notification" button.&lt;/p&gt;

&lt;p&gt;Nothing happened.&lt;/p&gt;

&lt;p&gt;No notification.&lt;/p&gt;

&lt;p&gt;No crash.&lt;/p&gt;

&lt;p&gt;No useful error message.&lt;/p&gt;

&lt;p&gt;At that point I made the same assumption I think most developers make:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I must have misconfigured APNs."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That assumption cost me hours.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Debugging Timeline
&lt;/h2&gt;

&lt;p&gt;Here's roughly how the next two days went.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;09:30&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Verified notification permissions.&lt;/p&gt;

&lt;p&gt;✅ Correct.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;10:00&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Verified APNs key.&lt;/p&gt;

&lt;p&gt;✅ Correct.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;11:15&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Regenerated provisioning profiles.&lt;/p&gt;

&lt;p&gt;No change.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;12:40&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Deleted and recreated the app on my phone.&lt;/p&gt;

&lt;p&gt;Still nothing.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;14:00&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Started blaming Firebase.&lt;/p&gt;

&lt;p&gt;Wrong direction.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;15:30&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Started questioning Capacitor.&lt;/p&gt;

&lt;p&gt;Also wrong.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;17:00&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Realized something important.&lt;/p&gt;

&lt;p&gt;TestFlight isn't your development environment.&lt;/p&gt;

&lt;p&gt;It's production.&lt;/p&gt;

&lt;p&gt;That changed everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Behaves Differently
&lt;/h2&gt;

&lt;p&gt;This was the biggest lesson.&lt;/p&gt;

&lt;p&gt;When you install from TestFlight, Apple expects production infrastructure.&lt;/p&gt;

&lt;p&gt;Not development.&lt;/p&gt;

&lt;p&gt;Not sandbox.&lt;/p&gt;

&lt;p&gt;Production.&lt;/p&gt;

&lt;p&gt;That sounds obvious now.&lt;/p&gt;

&lt;p&gt;It wasn't obvious while debugging.&lt;/p&gt;

&lt;p&gt;Once I started treating the build as a production deployment instead of "a beta version," the entire debugging strategy changed.&lt;/p&gt;




&lt;h1&gt;
  
  
  Then Another Problem Appeared
&lt;/h1&gt;

&lt;p&gt;Even after fixing the APNs environment...&lt;/p&gt;

&lt;p&gt;My backend occasionally crashed.&lt;/p&gt;

&lt;p&gt;The error looked something like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: read ECONNRESET

Error: write EPIPE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first I thought Apple was rejecting my requests.&lt;/p&gt;

&lt;p&gt;It wasn't.&lt;/p&gt;

&lt;p&gt;Apple was simply closing idle HTTP/2 sessions.&lt;/p&gt;

&lt;p&gt;That's normal.&lt;/p&gt;

&lt;p&gt;My server wasn't treating it as normal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Software Needs Production Recovery
&lt;/h2&gt;

&lt;p&gt;Once I stopped treating HTTP/2 disconnects as exceptional failures, the fix became straightforward.&lt;/p&gt;

&lt;p&gt;Instead of allowing dropped connections to crash the process, the backend now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detects disconnects&lt;/li&gt;
&lt;li&gt;Tears down the session cleanly&lt;/li&gt;
&lt;li&gt;Reconnects automatically&lt;/li&gt;
&lt;li&gt;Retries safely where appropriate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those few changes completely changed the reliability of the notification service.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Three Assumptions That Cost Me The Most Time
&lt;/h1&gt;

&lt;h2&gt;
  
  
  1. "If it works locally, production will work."
&lt;/h2&gt;

&lt;p&gt;Wrong.&lt;/p&gt;

&lt;p&gt;Development environments hide a surprising amount of complexity.&lt;/p&gt;

&lt;p&gt;Production exposes all of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. "It has to be my certificates."
&lt;/h2&gt;

&lt;p&gt;Certificates were fine.&lt;/p&gt;

&lt;p&gt;Provisioning profiles were fine.&lt;/p&gt;

&lt;p&gt;The issue was the production environment itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. "ECONNRESET means something is broken."
&lt;/h2&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;HTTP/2 sessions close.&lt;/p&gt;

&lt;p&gt;Networks reconnect.&lt;/p&gt;

&lt;p&gt;Production software should expect that.&lt;/p&gt;




&lt;h1&gt;
  
  
  What I Wish Someone Had Told Me
&lt;/h1&gt;

&lt;p&gt;Before shipping a Capacitor app with push notifications:&lt;/p&gt;

&lt;p&gt;✅ Test on a physical device early.&lt;/p&gt;

&lt;p&gt;✅ Treat TestFlight as production.&lt;/p&gt;

&lt;p&gt;✅ Don't hardcode APNs environments.&lt;/p&gt;

&lt;p&gt;✅ Log every push response.&lt;/p&gt;

&lt;p&gt;✅ Build reconnect logic into your APNs client.&lt;/p&gt;

&lt;p&gt;✅ Expect HTTP/2 sessions to terminate.&lt;/p&gt;

&lt;p&gt;✅ Document every native iOS change.&lt;/p&gt;

&lt;p&gt;These sound simple.&lt;/p&gt;

&lt;p&gt;They aren't obvious when you're learning from getting-started tutorials.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why I Built an Open-Source Reference
&lt;/h1&gt;

&lt;p&gt;While debugging this, I noticed something.&lt;/p&gt;

&lt;p&gt;Most tutorials stop once the first notification appears.&lt;/p&gt;

&lt;p&gt;That's the easy part.&lt;/p&gt;

&lt;p&gt;Almost nobody talks about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TestFlight failures&lt;/li&gt;
&lt;li&gt;Production APNs routing&lt;/li&gt;
&lt;li&gt;Backend reconnect logic&lt;/li&gt;
&lt;li&gt;Native configuration drift&lt;/li&gt;
&lt;li&gt;Production troubleshooting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are exactly the problems that consume entire weekends.&lt;/p&gt;

&lt;p&gt;So instead of letting those lessons disappear into Git commits, I started building an open-source project called &lt;strong&gt;Production Hybrid Reference&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The goal isn't another starter template.&lt;/p&gt;

&lt;p&gt;It's a collection of production-ready recipes, troubleshooting guides, and engineering postmortems based on real applications—not hypothetical examples.&lt;/p&gt;

&lt;p&gt;The first module focuses entirely on production push notifications and includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A runnable Node.js APNs server&lt;/li&gt;
&lt;li&gt;A Capacitor mobile example&lt;/li&gt;
&lt;li&gt;Troubleshooting guides&lt;/li&gt;
&lt;li&gt;A production postmortem&lt;/li&gt;
&lt;li&gt;Practical engineering principles&lt;/li&gt;
&lt;li&gt;Copy-paste reference code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it saves even one developer from spending two days chasing the wrong problem, it'll have been worth publishing.&lt;/p&gt;




&lt;h1&gt;
  
  
  What's Next
&lt;/h1&gt;

&lt;p&gt;Push notifications are just Module Zero.&lt;/p&gt;

&lt;p&gt;The next production battle scars I'll be documenting include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RevenueCat synchronization&lt;/li&gt;
&lt;li&gt;AppDelegate configuration drift&lt;/li&gt;
&lt;li&gt;Background tasks&lt;/li&gt;
&lt;li&gt;Production logging&lt;/li&gt;
&lt;li&gt;Apple review preparation&lt;/li&gt;
&lt;li&gt;Hybrid app architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each module follows the same philosophy.&lt;/p&gt;

&lt;p&gt;Real bug.&lt;/p&gt;

&lt;p&gt;Real investigation.&lt;/p&gt;

&lt;p&gt;Real fix.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;The biggest lesson wasn't technical.&lt;/p&gt;

&lt;p&gt;It was realizing that there's a huge gap between &lt;strong&gt;"it works"&lt;/strong&gt; and &lt;strong&gt;"it survives production."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tutorials are great at helping you build features.&lt;/p&gt;

&lt;p&gt;Production teaches you how software actually behaves.&lt;/p&gt;

&lt;p&gt;If you've run into a production issue with Capacitor that took hours—or days—to solve, I'd genuinely love to hear about it.&lt;/p&gt;

&lt;p&gt;Leave a comment below with your biggest production battle scar.&lt;/p&gt;

&lt;p&gt;If enough people are fighting the same problem, it'll become the next module in the Production Hybrid Reference.&lt;/p&gt;

&lt;p&gt;Let's save each other a few weekends.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;About this series:&lt;br&gt;
This article is the first in a series documenting real production issues I encountered while shipping a commercial Capacitor application. Every solution is based on an actual debugging session—not a contrived tutorial example.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
    </item>
  </channel>
</rss>
