DramaHub has been live on the Google Play Store since March 2026. 7,000+ downloads. 3,000+ daily active users. Built solo. ₹0/month infrastructure cost.
Today I found out I had been completely ignoring my users for 2 months.
The original setup
Both the "Suggest a Drama" and "Report a Problem" screens were Google Forms embedded in a Flutter WebView. Simple, fast to build, works on day one.
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.white)
..loadRequest(Uri.parse(_formUrl));
Responses went to a Google Sheet I never opened. No notification. No push alert. Nothing.
I had 1000+ drama suggestions and 1000+ problem reports sitting there completely unread.
Users were putting in effort to give me feedback. I was not receiving any of it.
The problems with the WebView approach
1. White background in a dark app
Google Forms load with a white background. My entire app is dark themed — #0D0D0D background. Every time a user opened these screens they got a jarring white flash. Unprofessional.
2. Zero notification
Responses go to a Google Sheet. Unless you manually open the sheet, you never know a submission happened.
3. No communication path
Users had no way to leave contact info. I had no way to follow up. Completely one-way.
4. No offline handling
If the form failed to load, users saw a blank white screen with no error message.
What I built instead
I replaced both screens with native Flutter forms — dark themed, matching the rest of the app exactly.
On submit, the form data is sent directly to a Telegram bot via the Telegram Bot API. The bot posts a formatted message to my private Telegram group instantly.
The Telegram message format
🚨 Problem Report 🕐 23 May 2026, 14:32
─────────────────────────────
⚠️ Type: Video not playing
🎥 Drama / Episode: Arafta Episode 56
📝 Description: Video not loading on wifi at all
─────────────────────────────
👤 Contact: @username
📱 App Version: 1.0.7
🌍 Country: IN
Full detail. Instant. Every time.
The TelegramService
class TelegramService {
TelegramService._();
static final TelegramService instance = TelegramService._();
String get _botToken => AppConfigService.instance.config.telegramBotToken;
String get _chatId => AppConfigService.instance.config.telegramChatId;
Future<bool> sendMessage(String message) async {
final uri = Uri.parse(
'https://api.telegram.org/bot$_botToken/sendMessage',
);
final response = await http.post(
uri,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'chat_id': _chatId,
'text': message,
'parse_mode': 'HTML',
}),
).timeout(const Duration(seconds: 10));
return response.statusCode == 200;
}
}
Key design decisions:
- Bot token and chat ID are never hardcoded — fetched from
app_config.jsonvia Cloudflare Worker at runtime - Admin can rotate the token anytime from the config — zero app update needed
- Uses existing
httppackage — no new dependency
Silent data attached to every submission
The user never sees these — they just appear in the Telegram message:
// App version — reads from pubspec.yaml automatically
final info = await PackageInfo.fromPlatform();
final appVersion = info.version; // "1.0.7"
// Country — from device locale, no permission needed
final locale = WidgetsBinding.instance.platformDispatcher.locale;
final country = locale.countryCode ?? 'Unknown'; // "IN"
Spam protection
// 1 hour cooldown between submissions per user
static const String _cooldownKey = 'report_problem_last_submit';
static const int _cooldownMinutes = 60;
Future<bool> _isCoolingDown() async {
final prefs = await SharedPreferences.getInstance();
final lastSubmit = prefs.getInt(_cooldownKey);
if (lastSubmit == null) return false;
final diff = DateTime.now().millisecondsSinceEpoch - lastSubmit;
return diff < Duration(minutes: _cooldownMinutes).inMilliseconds;
}
The security mistake I made
While setting this up, I made a mistake — I added the Telegram bot token directly to app_config.json in my public GitHub repository.
GitHub sent me a secret exposure alert within minutes.
{
"telegram_bot_token": "8940932851:XX",
"telegram_chat_id": "-51505959XX"
}
Anyone with read access to the repo could see the token and control the bot.
How I fixed it
Step 1 — Revoke immediately
Went to @botfather → /mybots → API Token → Revoke. New token generated in 10 seconds.
Step 2 — Make repo private
But wait — my Cloudflare Worker fetches from this GitHub repo to serve data to the app. Making it private would break everything.
The fix: add a GitHub PAT as an encrypted environment variable in Cloudflare Worker.
// In Cloudflare Worker — before:
githubResponse = await fetch(githubUrl, {
headers: { 'User-Agent': 'DramaHub-CDN/1.0' },
});
// After:
githubResponse = await fetch(githubUrl, {
headers: {
'User-Agent': 'DramaHub-CDN/1.0',
'Authorization': `token ${env.GITHUB_TOKEN}`,
},
});
env.GITHUB_TOKEN is stored as an encrypted secret in Cloudflare Worker settings — never in code, never visible.
Step 3 — Make repo private
With the Worker authenticated, the repo can be private. The full pipeline:
Flutter App
→ Cloudflare Worker (authenticated with GITHUB_TOKEN)
→ Private GitHub repo
→ Returns app_config.json with telegram_bot_token and telegram_chat_id
→ AppConfigService loads them at runtime
→ TelegramService uses them to send messages
Zero hardcoding anywhere. Admin panel can update the token via app_config.json — no app update needed.
The form UI
Both screens follow the same pattern — native Flutter, fully dark themed:
TextFormField(
style: AppTypography.body.copyWith(color: AppColors.white),
cursorColor: AppColors.primaryRed,
decoration: InputDecoration(
filled: true,
fillColor: AppColors.cardBackground, // #1C1C1C
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: AppColors.primaryRed, width: 1.5),
),
),
)
Success state replaces the form entirely after submission — no snackbar, no dismissable toast:
// After successful submit:
setState(() => _submitted = true);
// Build method:
body: _submitted ? _buildSuccessState() : _buildForm(),
What changed for users
| Before | After |
|---|---|
| White Google Form in dark app | Native dark themed form |
| Submissions silently disappear | Instant Telegram notification |
| No contact option | Optional Telegram/email field |
| No error handling | Proper offline error state |
| No success feedback | Full success screen |
The real lesson
I was so focused on building new features that I never went back to check if existing ones were actually working.
A 3,000 DAU app is not a side project. Real people use it every day. They deserve to be heard.
Now they are.
DramaHub is live on Google Play Store. Built solo. ₹0/month infrastructure.
Architecture: Flutter + GitHub as database + Cloudflare Workers CDN + Firebase
Top comments (0)