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)