Nullgram is a fully custom Telegram client built from scratch in Flutter, without any of the messy original Telegram code.
The goal is to create a clean, modern, and performant alternative to the official app.
Currently, Nullgram is being developed only for Android.
If the author ever gets an iPhone - iOS support might become a thing too.
The author doesn’t like what Telegram has become -
bloated with ads, Telegram Premium, and all the NFT-style scam nonsense.
So instead of complaining, Nullgram is being built as a clean and free alternative,
where the focus is on simplicity, and functionality - not monetization.
Chats
- Chat list with folders, archive, pinning, muting, mark as read/unread
- Unread counters, mention badges, draft previews, delivery/read ticks
- Live typing indicators, connection status, unread divider in history
Messages
- Text with MarkdownV2 formatting, spoilers, tappable links and mentions
- Replies with quoted preview, forwarding, editing, pinning, reactions
- Multi-select for bulk copy / forward / delete (for me or for everyone)
- Sending photos, albums, videos, documents, polls and contacts
- Voice messages: hold to record, slide to cancel, real waveforms
- Round video messages, and fullscreen playback for every video
- Stickers, including animated (TGS), plus saved GIFs
- Custom (premium) emoji in text, reactions and emoji statuses
- Silent send, "send when online" and scheduled messages
- Bot inline keyboards, callback buttons and the bot's command menu
- Per-chat message search, shared-media browser, jump-to-message
- Message translation, reaction and read-receipt lists
- Per-chat auto-delete timer
People and groups
- Sign-up for a new phone number, including the Terms of Service
- Contacts, group and channel creation, invite links, join by link
- Member lists with promote, dismiss and remove; group name, photo, about
- Secret (end-to-end encrypted) chats
- Profiles with bio, username, phone, shared actions and blocking
- Global search across chats, public chats and message text
Calls
- 1:1 voice and video calls, plus a call log
Accounts
- Unlimited accounts, all online at once: an account that is not on screen still delivers notifications and unread badges
- Switching from the drawer header, with no re-login and nothing re-downloaded
Settings
- Material You theming, light/dark/AMOLED
- Interface language (English and Russian), which also moves TDLib's own language pack
- Passcode lock with biometric unlock and an auto-lock delay
- MTProto/SOCKS5/HTTP proxies, with ping and
t.me/proxylink import - Automatic media download limits per connection, mobile data and Wi-Fi
- Notification defaults per chat scope, blocked users, two-step verification, active devices, storage usage and cache clearing
- Forum topics
- Creating and editing chat folders (existing folders are shown)
- Sticker pack browsing and installing
- Group voice and video chats
- Live location sharing
- Custom chat wallpapers
- Channel comment threads
- Inline bot queries (
@bot query) - Stories
Roaming is not a separate automatic-download profile: nothing in the plugin layer distinguishes it from ordinary mobile data.
Signing in needs an api_id of your own from my.telegram.org. With official
Telegram application credentials the server demands a Play Integrity or
reCAPTCHA token (updateApplicationRecaptchaVerificationRequired) and only
accepts one issued to the official app, so no other package can ever complete
a new sign-in. The app aborts that verification and says so on the login
screen rather than waiting silently.
An incoming call reaches only the account on screen. TDLib file ids belong to the client that issued them, so an account that has never been on screen shows an initial rather than an avatar in the switcher until its photo is on disk.
- ✅ Android (in development)
- ❌ iOS (maybe someday…)
-
Clone the repository
git clone https://github.com/dima-xd/nullgram.git cd nullgram -
Copy
assets/config/env.exampletoassets/config/envand fill in yourAPI_ID,API_HASHandDB_ENCRYPTION_KEY. That file is git-ignored and read only when a build passes no credentials of its own. -
Get dependencies
flutter pub get
-
Run the app
flutter run
A release build takes the same three values as compile-time defines, so they never have to be committed:
flutter build apk --release --split-per-abi \
--dart-define=API_ID="$API_ID" \
--dart-define=API_HASH="$API_HASH" \
--dart-define=DB_ENCRYPTION_KEY="$DB_ENCRYPTION_KEY"The workflow in .github/workflows/build.yml reads them from the repository
secrets of the same names and fails before building when one is missing. A
define always wins over assets/config/env.
DB_ENCRYPTION_KEY is not interchangeable: an installed app cannot read a
database written with a different key, so changing it signs every account out.
The core client is usable day to day. Contributions, ideas, and pull requests are welcome.