Blog
Build in Public
March 26, 20268 min

Quality sprint: fixing the silent bugs that lead to uninstalls

A reminder that vanishes without warning, an email that never arrives, a phantom error at startup: these bugs don't make headlines in a changelog, but they destroy your users' trust. In one quality sprint day, I fixed four silent issues in TAMSIV — my voice task manager for Android — and the daily experience has radically changed.

Key takeaways:
- Silent bugs (deleted reminders, muted emails, flash errors) cause more uninstalls than visible crashes.
- Activating push AND email by default for reminders eliminates 90% of "I wasn't notified" tickets.
- A quality sprint without new features is the most profitable investment for user retention.
- Complete translation of every feature (here, badges in 6 languages) is a sign of respect for international users.
Developer working at night in front of code screens, warm desk lamp ambiance
Quality sprints often happen late at night, when you can finally focus on the details that matter.

Why is a quality sprint more important than a new feature?

When you develop an app solo, the temptation is constant: add features. A new filter here, an integration there. It's gratifying, easy to show in screenshots, makes for a nice commit.

But the reality is that users don't uninstall an app because a feature is missing. They uninstall because a reminder didn't work. Because an email never arrived. Because an incomprehensible error appeared at startup.

According to a study by UserTesting, 88% of users never return after a bad experience. And silent bugs — those that don't crash the app but erode trust — are the most dangerous. You don't get a crash report. The user doesn't report anything. They just leave.

That's why I decided to dedicate an entire day to a 100% quality sprint. No new features. No ambitious refactoring. Just four surgical fixes that, put together, truly change the daily experience of TAMSIV.

How can valid reminders disappear silently?

The scenario: you create a task with three reminders — one in 10 minutes, one tomorrow morning, one Friday. Except the first reminder is already in the past (you took too long to validate). TAMSIV displayed a warning: "This reminder is in the past." So far, logical.

The problem? By closing this warning, all reminders were deleted. The two future, perfectly valid reminders went with it. A slightly overzealous cleanup in the validation code.

This kind of bug is particularly insidious. The user doesn't even know their reminders have disappeared. They wait for tomorrow morning's notification... which will never come. And they'll blame the app — rightly so.

The technical fix

The solution involved clearly separating past reminders from future reminders in the validation logic. Specifically:

  • Time filter: Only reminders with a date earlier than Date.now() are marked as expired.
  • Targeted warning: The alert message only concerns reminders actually in the past, with their exact number.
  • Guaranteed retention: Future reminders remain intact, no matter what the user does with the warning.
  • Time zones: I took the opportunity to strengthen time zone management in validation — an edge case I had underestimated.

The reminders and recurrence system is a pillar of TAMSIV. A bug here, even a minor one, has a disproportionate impact on user trust.

Why were reminder emails never arriving?

TAMSIV supports two notification channels for reminders: push (phone notification) and email. In theory. In practice, when a user created a reminder, only the push channel was active by default. Email? Disabled. Silently.

Smartphone displaying email and push notification icons on a dark background
Push and email: both channels must be active by default to cover all usage contexts.

If you didn't manually check "email" in the reminder settings, you would never receive anything in your inbox. No error, no message — just silence. And for a user who doesn't systematically check their phone, that's a completely missed reminder.

What's the right default for notification channels?

The question seems trivial, but it's not. As the Nielsen Norman Group recommends in its research on default values, the default should correspond to what the majority of users expect. And the majority expects to be notified — through all available channels.

Now, both channels are active by default. You receive a push and an email. If you want to disable one of them, it's still possible, but the default behavior is what everyone expects. This is the principle of a sane default — a central concept in the design of productivity applications.

Where did this "Invalid Refresh Token" error at startup come from?

This one is the kind of bug that doesn't break anything but undermines trust. You open the app after a few hours of inactivity, and for a fraction of a second, an "Invalid Refresh Token" error flashes on the screen. Then everything works normally.

Imagine: you open TAMSIV in the morning to see your tasks for the day, and the first thing you see is an error. You don't know what it means. You don't know if your data is safe. You don't know if the app is working correctly. Even if everything is perfectly OK in reality, the impression is disastrous.

The technical analysis of the problem

On a cold start, Supabase Auth tried to refresh the authentication token. If the token had expired (after a few hours of inactivity), the SDK threw an error before the automatic reconnection mechanism had time to do its job.

The error propagated to the UI when it had no reason to be there — the reconnection always ended up succeeding. This is a classic pattern in apps using JWT tokens with automatic refresh: the first call fails, the refresh is triggered, the second call succeeds.

The fix intercepts this specific error (AuthApiError with the code invalid_refresh_token) at the right level and removes it from the display. Session refreshing continues to work exactly as before, but the user no longer sees an anxiety-inducing message that doesn't concern them.

This type of error handling is critical for any app using Supabase authentication. The SDK does its job well — you just need to avoid exposing its intermediate errors to the user.

How to translate a gamification system into 6 languages?

TAMSIV is available in French, English, German, Spanish, Italian, and Portuguese. The gamification system — levels, badges, streaks — is one of the features that users discover gradually. Except the explanatory guide for badges only existed in French and English.

Globe surrounded by flags and symbols representing six different languages
Translating every element of the interface is an investment that pays off in the long run.

This is a more subtle problem than it seems. A German-speaking user who discovers the badge system and encounters text in French or English will immediately feel that this part of the app is unfinished. This is a very strong negative signal, especially when the rest of the interface is correctly translated.

The internationalization process for badges

TAMSIV's 10 badges each have a name, a description, and unlock conditions. That's 30 text strings to translate into 4 additional languages (German, Spanish, Italian, Portuguese). The 6-language internationalization system I implemented handles this with an automatic translation pipeline via OpenRouter, followed by a manual review for gamification terms.

It's not a spectacular fix, but it's the kind of detail that makes a Portuguese or German-speaking user feel at home in the app. And as CSA Research explains, 76% of consumers prefer to buy products in their own language.

What is the real impact of these fixes on retention?

These four fixes represent a day's work. None would have made an exciting changelog title. But together, they eliminate real friction:

  • Lost reminders: No more valid reminders disappear after a warning about an expired reminder.
  • Missed emails: Both notification channels are active by default, no more surprises.
  • Startup error: The "Invalid Refresh Token" message never appears again.
  • Translated gamification: All 10 badges are available in the 6 supported languages.

In terms of retention, each of these bugs was an invisible point of friction. The user doesn't report "your reminder email didn't arrive" — they simply think the app isn't working and uninstall it. According to Adjust, the average app retention rate at D30 is 6%. Every friction eliminated pushes this number up.

How to organize a quality sprint when you're a solo developer?

Being a solo developer on a project like TAMSIV means no one else will plan a quality sprint for you. Here's the method I use:

  1. Use your own app daily — dogfooding is the best source of bugs. Every irritation noted in a voice memo (with TAMSIV, of course).
  2. Prioritize by user impact — a bug that affects 100% of users at every startup takes precedence over a bug that affects 2% of cases.
  3. Limit the scope — one day, maximum 4 fixes. No tempting refactoring along the way.
  4. Test on real devices — the 12 beta testers are there for that. A bug reproduced on 3 different devices is a real bug.

The zero-feature sprint I did the following week confirmed this approach: feedback from beta testers was much better after these silent fixes than after adding new features.

Frequently Asked Questions

How long does an effective quality sprint take?

A focused day is enough for 3 to 5 targeted fixes. The important thing is not to mix it with feature development — the mental context is different. I recommend a quality sprint every 2 weeks for a project in its launch phase.

How to detect silent bugs when there are no crash reports?

Daily dogfooding is the number one method. Use your own app like a normal user, not like a developer. Tools like Firebase Analytics also help detect abnormal patterns (very short sessions, unused features).

Why activate push AND email by default rather than letting the user choose?

Because the majority of users never change default settings (Nielsen Norman Group research). Opt-out is more respectful than opt-in for reminder notifications — the user has explicitly asked to be reminded, they expect to receive the notification on all available channels.

Is the "Invalid Refresh Token" error dangerous for data?

No, absolutely not. It's a normal intermediate error in the JWT token refresh flow. Supabase Auth handles this automatically — the expired token is replaced by a new one in the background. The problem was only the display of this technical error to the user.

How to manage the translation of gamification terms (badges, levels)?

I use an automatic translation pipeline via OpenRouter, followed by a review for gaming-specific terms. Some terms like "streak" or "badge" are often kept in English even in localized versions, as they are universally understood by mobile app users.