DEV Community

byArto
byArto

Posted on

I Built a Subscription Tracker That Works as Both a PWA and a Telegram Mini App

A few months ago I had a simple problem: too many subscriptions, zero visibility into what I was actually paying. The standard fixes (spreadsheet, notes app) never stuck.

So I built Subeasy. A subscription tracker that lives inside Telegram and also works as a standalone PWA. Same codebase, two platforms

Here's what the build actually looked like

The stack

• Next.js 16 (App Router)
• React 19 + TypeScript 5
• Tailwind CSS 4
• Framer Motion for animations
• Supabase (PostgreSQL) for cloud sync
• Recharts for analytics
• Vercel for deploys

Nothing exotic. The interesting part isn't the stack, it's the decisions around data and platform

Offline-first, always

The biggest design decision early on: localStorage as the source of truth, Supabase as the sync layer.

Every subscription lives in localStorage first. The app fully works without a network connection or even an account. When the user logs in, it syncs to Supabase.

The sync strategy: full pull from remote, merge (remote wins on conflicts), full push. Debounced at 1 second

Why? Most apps require signup before you can do anything. That kills conversion. With offline-first, the user adds 5 subscriptions and sees their monthly total before touching an account screen

Dual platform from one codebase

The TelegramProvider wraps the entire app:

const tg = window.Telegram?.WebApp
if (tg) {
tg.ready()
tg.expand() // Fullscreen mode
}

HapticFeedback on every interaction — taps, submissions, errors. That tactile feedback makes it feel native rather than a webpage in a webview.


BackButton - the detail that matters most

Telegram's back button needs to close modals in the right order. If a user has a detail screen open and taps back, it should close that modal, not exit the app.

useEffect(() => {
const hasOpenModal = showAddModal selectedSubId editingSubId showSearch showNotifications
if (hasOpenModal) {
tg.BackButton.show()
tg.BackButton.onClick(handleBackButton)
} else {
tg.BackButton.hide()
}
}, [showAddModal, selectedSubId, ...])

Getting this wrong feels broken. Getting it right feels invisible — which is exactly what you want

otifications without a bot

Telegram Mini Apps can't push notifications the way a regular bot can. So notifications run through Service Worker + Web Notifications API.

Works well on Android. On iOS we fall back to an in-app notification panel. The road forward is Telegram's native notification features - they keep expanding what Mini Apps can do.


Exchange rates via Edge Function

export const runtime = 'edge'

export async function GET() {
const res = await fetch('https://www.cbr.ru/scripts/XML_daily.asp')
return Response.json({ rate, updatedAt: new Date() })
}

Cached in localStorage for 1 hour. Currency toggle is instant, no loading spinner.


The Sub Score

A 0-100 rating of subscription health:

• Budget (25 pts) - % of income on subscriptions
• Activity (25 pts) - active vs inactive ratio
• Duplicates (20 pts) - same category, multiple services
• Diversification (20 pts) - one category eating 80%+ of budget
• Trials (5 pts) - unconverted trials about to charge
• Annual plans (5 pts) - using annual pricing where it saves money

It doesn't tell users what to do. It makes the situation visible. People see a C and want to fix it. That's enough


i18n at 380+ keys

Full Russian and English. Custom useLanguage() hook, t('key') function.

Main lesson: build the translation structure before writing any UI copy. Retrofitting i18n into existing components is painful.


What's next (PRO tier)

Free version stays fully functional with no subscription limits - deliberate product decision.

PRO will add:

• Themes and accent colors
• Multi-currency (EUR, GBP, TRY, KZT, AMD)
• Price history tracking
• PDF/CSV export
• AI insights ("you're spending 3x the average on streaming")
• Family plan (shared workspace, 2-6 people)
• Payment via Telegram Stars - no Stripe, no acquiring needed

That last one is a natural fit for a Mini App already inside Telegram.


Try it

App: t.me/Subeasyapp_bot/subeasy
Web: www.subeasy.org

Free, no bank connections. You add subscriptions manually. Turns out people prefer it that way

Top comments (0)