Everything worked perfectly... until I uploaded my app to TestFlight.
Every push notification tutorial I read covered the same things:
- Register for push notifications.
- Request permission.
- Get the device token.
- Send your first notification.
And to be fair, all of those tutorials worked.
Until they didn't.
The moment I uploaded my Capacitor app to TestFlight, the entire push notification flow quietly fell apart.
No obvious errors.
No compiler issues.
No helpful stack traces.
Just... silence.
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.
This article is the postmortem I wish I'd found before I started.
The Setup
The stack was pretty standard.
- Capacitor
- TypeScript
- Node.js backend
- APNs
- iOS
Everything behaved exactly as expected during development.
Push permissions appeared.
The device registered.
Tokens looked correct.
Notifications arrived instantly.
I genuinely thought this part of the app was finished.
Then I created a TestFlight build.
The Moment Everything Broke
I installed the TestFlight build.
Opened the app.
Tapped my "Send Test Notification" button.
Nothing happened.
No notification.
No crash.
No useful error message.
At that point I made the same assumption I think most developers make:
"I must have misconfigured APNs."
That assumption cost me hours.
My Debugging Timeline
Here's roughly how the next two days went.
09:30
Verified notification permissions.
✅ Correct.
10:00
Verified APNs key.
✅ Correct.
11:15
Regenerated provisioning profiles.
No change.
12:40
Deleted and recreated the app on my phone.
Still nothing.
14:00
Started blaming Firebase.
Wrong direction.
15:30
Started questioning Capacitor.
Also wrong.
17:00
Realized something important.
TestFlight isn't your development environment.
It's production.
That changed everything.
Production Behaves Differently
This was the biggest lesson.
When you install from TestFlight, Apple expects production infrastructure.
Not development.
Not sandbox.
Production.
That sounds obvious now.
It wasn't obvious while debugging.
Once I started treating the build as a production deployment instead of "a beta version," the entire debugging strategy changed.
Then Another Problem Appeared
Even after fixing the APNs environment...
My backend occasionally crashed.
The error looked something like this.
Error: read ECONNRESET
Error: write EPIPE
At first I thought Apple was rejecting my requests.
It wasn't.
Apple was simply closing idle HTTP/2 sessions.
That's normal.
My server wasn't treating it as normal.
Production Software Needs Production Recovery
Once I stopped treating HTTP/2 disconnects as exceptional failures, the fix became straightforward.
Instead of allowing dropped connections to crash the process, the backend now:
- Detects disconnects
- Tears down the session cleanly
- Reconnects automatically
- Retries safely where appropriate
Those few changes completely changed the reliability of the notification service.
The Three Assumptions That Cost Me The Most Time
1. "If it works locally, production will work."
Wrong.
Development environments hide a surprising amount of complexity.
Production exposes all of it.
2. "It has to be my certificates."
Certificates were fine.
Provisioning profiles were fine.
The issue was the production environment itself.
3. "ECONNRESET means something is broken."
Not necessarily.
HTTP/2 sessions close.
Networks reconnect.
Production software should expect that.
What I Wish Someone Had Told Me
Before shipping a Capacitor app with push notifications:
✅ Test on a physical device early.
✅ Treat TestFlight as production.
✅ Don't hardcode APNs environments.
✅ Log every push response.
✅ Build reconnect logic into your APNs client.
✅ Expect HTTP/2 sessions to terminate.
✅ Document every native iOS change.
These sound simple.
They aren't obvious when you're learning from getting-started tutorials.
Why I Built an Open-Source Reference
While debugging this, I noticed something.
Most tutorials stop once the first notification appears.
That's the easy part.
Almost nobody talks about:
- TestFlight failures
- Production APNs routing
- Backend reconnect logic
- Native configuration drift
- Production troubleshooting
Those are exactly the problems that consume entire weekends.
So instead of letting those lessons disappear into Git commits, I started building an open-source project called Production Hybrid Reference.
The goal isn't another starter template.
It's a collection of production-ready recipes, troubleshooting guides, and engineering postmortems based on real applications—not hypothetical examples.
The first module focuses entirely on production push notifications and includes:
- A runnable Node.js APNs server
- A Capacitor mobile example
- Troubleshooting guides
- A production postmortem
- Practical engineering principles
- Copy-paste reference code
If it saves even one developer from spending two days chasing the wrong problem, it'll have been worth publishing.
What's Next
Push notifications are just Module Zero.
The next production battle scars I'll be documenting include:
- RevenueCat synchronization
- AppDelegate configuration drift
- Background tasks
- Production logging
- Apple review preparation
- Hybrid app architecture
Each module follows the same philosophy.
Real bug.
Real investigation.
Real fix.
Final Thoughts
The biggest lesson wasn't technical.
It was realizing that there's a huge gap between "it works" and "it survives production."
Tutorials are great at helping you build features.
Production teaches you how software actually behaves.
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.
Leave a comment below with your biggest production battle scar.
If enough people are fighting the same problem, it'll become the next module in the Production Hybrid Reference.
Let's save each other a few weekends.
About this series:
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.
Top comments (0)