The Flutter client for Elephant, a privacy-first messenger. Android is the primary target, and release APKs are published on GitHub Releases. The app ID is in.commandlinecoding.elephant.
Requirements: a Flutter SDK with Dart ^3.12.0, plus the Android SDK. CI uses Java 25 (Temurin) for Android builds.
cd mobile
flutter pub get
flutter runThe app connects to the public server elephant.commandlinecoding.in:443 by default. To use a local or self-hosted server, tap "Connected to: … ⚙️" on the login screen and enter a host and port. The values are stored in secure storage.
- Port
443(or any*.commandlinecoding.inhost) useshttps:///wss://. Every other port uses plainhttp:///ws://. - From the Android emulator, the host machine is reachable at
10.0.2.2, e.g. host10.0.2.2, port3000.
flutter build apk --release --split-per-abi --no-tree-shake-icons # per-ABI APKs
flutter build apk --release --no-tree-shake-icons # universal APKThe android-release workflow runs the same commands on every v* tag and attaches the APKs to the GitHub Release.
The launcher icon is generated from assets/launcher/elephant.png. The configuration is in pubspec.yaml under flutter_launcher_icons and launcher_name.
dart run flutter_launcher_iconslib/
├── main.dart # App bootstrap and provider setup
├── core/constants.dart # Server host/port (Env) and URL builders
├── controllers/
│ ├── auth_state.dart # Login/register/OAuth deep-link handling, session state
│ └── chat/ # Inbox, active chat, connection, search, group details
├── models/ # User, Message, Conversation, Group, InboxItem, WS event
├── pages/ # auth, home, chat, new chat, settings screens
├── providers/ # Group controller and timer providers
├── services/
│ ├── api_services.dart # Dio client with automatic token refresh
│ ├── auth_service.dart # Token storage (flutter_secure_storage)
│ ├── ws_service.dart # WebSocket connection, heartbeat, outgoing events
│ ├── chat/chat_event_handler.dart # Incoming WS events → local DB + UI
│ ├── chat/chat_sync_service.dart # Replays the offline action queue
│ ├── db_services.dart # SQLCipher-encrypted local database
│ ├── signal_service.dart # Signal Protocol keys, sessions, encrypt/decrypt
│ └── sqlite_signal_store.dart # Signal key/session store backed by the local DB
├── themes/ # Light/dark themes and theme provider
└── widgets/ # Shared chat and home widgets
- Username/password login and registration go through
/api/auth/*. Access and refresh tokens are stored influtter_secure_storage. ApiServicerefreshes the access token automatically when it expires. Concurrent requests share a single refresh call.- Google/GitHub sign-in opens
/api/auth/{provider}in the browser. The server redirects back toelephant://oauth-callback?access_token=…&refresh_token=…, andapp_linkspicks that up.
- Messages are written to the local database first (
sync_status = pending) and added toaction_queue. They are sent over the WebSocket when connected. ChatSyncServicereplays queued actions on reconnect. Each action is retried up to 5 times. The client generates the message UUID, so the server stores the message under the same ID.- The local database is encrypted with SQLCipher. Its 32-byte random key is kept in secure storage. If the keystore becomes unreadable, the local database is wiped and recreated.
- On login, if no keys exist on the device yet, the app generates a Signal identity key pair, a signed prekey and 100 one-time prekeys. It uploads the public halves to
/api/e2ee/keyswithdevice_id = "main". - To message a contact for the first time, the app fetches the contact's bundle from
/api/e2ee/bundle/{userId}and builds a session (X3DH). Later messages use the Double Ratchet. - Direct message
contentis sent as{"type": <signal message type>, "ciphertext": "<base64>"}. - Group messages are not yet end-to-end encrypted. They are currently sent as
{"type": 0, "ciphertext": "<plaintext>"}. Sender Key helpers (encryptGroupMessage,processSenderKeyDistribution) exist inSignalServicebut aren't wired into sending yet. - The Privacy and security settings screen can reset all sessions to recover from decryption errors.
See docs/API_CONTRACTS.md for the full server protocol.