- TypeScript 79.7%
- Rust 6.3%
- Kotlin 4.6%
- JavaScript 3%
- Python 2.8%
- Other 3.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| docker | ||
| docs | ||
| flatpak | ||
| libtacky@8c3fa4d54d | ||
| src-tauri | ||
| tools | ||
| ui | ||
| worker | ||
| .gitignore | ||
| .gitmodules | ||
| CHANGELOG.md | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| webrtc.mk | ||
anotherim-js
Another IM: an XMPP messenger with one UI, written in TypeScript and lit, and three ways to run it. Each of them is a host of libtacky's JSON wire API, and none of them speaks XMPP itself: the protocol, the local store, end-to-end encryption, calls and file transfer are libtacky's.
- The browser client: libtacky compiled to wasm with Emscripten, running
in a Web Worker (
worker/); the page reaches it overpostMessage. - The desktop apps (Linux, Windows): a Tauri 2 shell (
src-tauri/) that linkslibtacky.aand runs the backend natively. - The Android app: the same Tauri shell built for Android, with a Kotlin
half for push, calls and files (
src-tauri/gen/android/).
Version 0.4.7, alpha: breaking changes may still be made. Licence MPL-2.0
(LICENSE); the libtacky it embeds is GPL-3.0.
Architecture
ui/ TypeScript + lit: views, state, the wire API
│ one Transport interface, three implementations
┌───────────┼────────────┐
mock worker tauri
in-page libtacky as libtacky.a in the Tauri shell
(dev) wasm (browser) (desktop, Android)
| component | language | what it is | code | tests | coverage |
|---|---|---|---|---|---|
ui/ |
TypeScript | the client: views/ (lit components, 23,158), state/ (one controller per feature, 13,257), protocol/ (frames, the transports, the in-page mock backend, 5,794), push/ (the service worker, 367), boot and i18n (946) |
43,762 | 29,894 | 90.3% of lines |
src-tauri/ |
Rust | the Tauri shell: the binding over libtacky's tacky.h, the file bridge, D-Bus notices, UnifiedPush, the system tray, the XML console windows, the Android call surface's Rust half |
3,640 | 1,174 | 95.6% of the lines of its 15 plain-std modules; the binding and the Tauri glue not measured |
src-tauri/gen/android/ |
Kotlin | the Android half: push, notifications, the call surface, Save/Share/Open of files, the network watch | 2,406 | 1,582 | not measured |
worker/ |
C, JS, Tcl, make | the browser backend: Tacky booted under Emscripten behind a postMessage pipe |
1,756 | 1,244 | 86.9% of the lines of web/ |
| total | TypeScript 43,762, Rust 3,651, Kotlin 2,406, JS 907, C 603, make/TOML/Tcl 235 | 51,564 | 33,894 | ||
| everything else | Python, JS, sh, make | tools/ (the smoke tests, the i18n extractor, the icon scripts), docker/, flatpak/, the Makefiles; not in the total |
4,473 | -- | -- |
code: lines of code, without blank lines, comments and generated files (the
wire API generated from libtacky's schema, ui/src/protocol/generated/api.ts,
10,232 lines, is not counted); the total is the four components', by
language, and leaves out everything else. tests: the lines of test code
(ui/test/, src-tauri/tests/, the Rust #[cfg(test)] modules, the Kotlin
src/test/, worker/test/). Counted on 2026-10-05 at libtacky
v1.0.0-alpha3 by the rules of libtacky's tools/stats.py (its file_counts:
the files git tracks; Markdown, JSON, CSS, HTML, images and lockfiles are not
code, so the seven language catalogues are not counted). coverage: line
coverage measured on 2026-10-05 in the image. make coverage-ui runs the UI's
tests under V8's coverage (@vitest/coverage-v8) over every ui/src file
but the generated wire API: 29,652 of 32,827 lines, 85.2% of branches, 90.8%
of functions. make coverage-shell builds the shell's plain-std modules (the
ones make test-shell runs) with LLVM's instrumentation: 1,717 of 1,797
lines, from 87.2% (console_windows.rs) to 100%. The worker's own JS was
measured once under its seven node suites with node's NODE_V8_COVERAGE:
252 of 290 lines of web/ (tacky-worker.js 93.1%, store.js 84.2%, whose
IndexedDB path node cannot run). The Kotlin and the parts of the shell that
link libtacky.a have no coverage run.
The language packs: docs/i18n.md. Push and notifications: docs/push.md. Whether a message reaches a closed app: docs/delivery.md. Calls on Android: docs/android-calls.md. How a feature is added, and what each one sends on the wire: docs/porting-from-qt.md.
Supported features
Status, per feature, from the user's point of view:
- 🔴 TODO: not implemented yet.
- 🟡 WIP: implemented but not finished (parts missing, a platform without it, known bugs) or not yet tried on the devices it is for.
- 🔵 MOSTLY: implemented and its tests pass, but not verified by hand against real servers and clients.
- 🟢 DONE: verified and stabilised. Only a maintainer sets this, by hand.
The statuses come from a code-and-test audit of ui/src, src-tauri and
ui/test on 2026-10-01 at 36b7367. The UI's tests run against a fake
transport and the in-page mock backend, not against libtacky or a server, so
a feature is no further along than the libtacky rows it stands on (libtacky's
README, Supported features).
| Feature | Description | Status |
|---|---|---|
| Sign in | XMPP accounts. The address first and the server's own sign-in page in the browser (OAuth: XEP-0493 on Prosody, ejabberd's own page and X-OAUTH2; the loopback redirect on the desktop, im.another.messenger:/oauth on Android; a browser tab signs in with the password), or, under Sign in with options, username and password on another.im or any other server with its host, port and TLS by hand; the provider changeable, another.im by default; the welcome screen in the theme in force; several accounts, one in front at a time as in Telegram (the account menu lists them with their unread counts and switches between them, the chat list shows the chats of the one in front, and every account's messages notify), each enabled or removed from its menu and disabled on its page; the connection's state and errors on the account. On Android the shell tells libtacky itself when the network comes, goes or is blocked for the app and when the app goes to the background (XEP-0352), so a backgrounded app holds instead of redialling. In a browser the server must offer a WebSocket (RFC 7395). |
🔵 MOSTLY |
| WebSocket discovery | The browser's way in. The endpoint is read from the domain's host-meta by libtacky (XEP-0156) or typed under Advanced; the browser path is untested and falls back to wss://<domain>/xmpp-websocket. |
🟡 WIP |
| Sign up | In-band registration. The server's form with its own Create account button, CAPTCHA images and proof-of-work (preferred when the server offers both, the picture on request); a server certificate that does not verify is asked about first (in the apps). | 🔵 MOSTLY |
| SSO sign-in | Organisation sign-in. Shown only where a broker is configured (the build's SSO_BROKER_URL or the setting); through it or another one, PKCE, then the profile set-up sheet; a build can offer it alone (AUTH_SOURCES=sso). |
🔵 MOSTLY |
| Device link | Log in from another device. The device-link code or QR, scanned with the camera or pasted, from the welcome screen's Sign in with a QR code; the two devices' verifying each other is said once libtacky reports it. | 🔵 MOSTLY |
| Chats | One-to-one chats. History paging both ways, catch-up from the archive, delivery ticks and read markers, who is typing in the chat header and over the chat list row's preview, per-chat drafts kept and synced, pinned chats, closing a chat (Ctrl+W) until its next message, sort and filter, keyboard navigation. | 🔵 MOSTLY |
| Message actions | Reply, edit, delete, react, forward. Replies with a quote and a jump to it, edits of our own messages, deletion (our own, in 1:1 chats), six quick reactions and chips that toggle, forwarding to several chats, multi-select and copy, retry and send without encryption, the stanza's XML. | 🔵 MOSTLY |
| Message styling | XEP-0393. Bold, italic, strike, code and quotes drawn from libtacky's tree, behind a switch; the WebKitGTK smoke finds the styled spans in the desktop engine. | 🟡 WIP |
| Search | In the chat and across the account. The local store, the server's archive (MAM full text, where offered) or both; hits highlighted and stepped through. | 🔵 MOSTLY |
| Threads | XEP-0201. "Reply in thread", the thread pane, the chat's thread list. Files go to the timeline's composer, not the pane's. | 🔵 MOSTLY |
| Saved messages | The chat with yourself, always the first row of the chat list, above pinned chats and whatever the sort, and first in the chat picker. | 🔵 MOSTLY |
| Contacts | The roster. Contact requests, add, rename and remove, contact details with each online device on a line of its own, the subscription asked for, withdrawn or revoked in either direction, presence and status messages, the last time a contact was active (XEP-0319) deciding their dot and, without a status text, their line -- online (green) under five minutes, while typing or in the chat (XEP-0085), "Last seen 3 hours ago" (yellow) under a day, "Last seen …" (red) after, and "Last seen recently" (between green and yellow) when no time is known -- read from the device they used last, blocking, an optional "contacts first" gate before a stranger can write. | 🔵 MOSTLY |
| Invitations | XEP-0379. An invitation link and its QR code; opening one asks, then adds the contact. | 🔵 MOSTLY |
| Find people | Directory search and server commands. The server's user directory (XEP-0055) and ad-hoc commands (XEP-0050) with data forms. | 🔵 MOSTLY |
| Gateways | Transports. The transports browser, and phone numbers and other foreign addresses resolved to a gateway. Registering with a gateway is only through its ad-hoc commands (libtacky's XEP-0100 row). | 🟡 WIP |
| Rooms | Group chats (MUC). Join with room discovery, bookmarks with autojoin, occupants with roles and presence, subject, invitations, kick, ban, roles and memberships, voice requests, destroy, room avatars. | 🔵 MOSTLY |
| OMEMO | End-to-end encryption, OMEMO 0.3. Per 1:1 chat, on by default and off by default for a contact that announces no OMEMO device (a transport, say). The padlock and Contact info's single row open the Encryption information window: why the chat is or is not encrypted, their devices and ours with fingerprints, trust and when each was last seen, key verification by QR, removing one of our own devices, Disable behind a confirm and Enable without; a line over the composer says when a chat is not encrypted. Blind trust. A members-only, non-anonymous room has the same padlock, line and window, with its members' devices and who stopped a send that failed; OMEMO 2 is switched off in libtacky in this release. | 🟡 WIP |
| Files | Attachments. A tray per chat (pick, paste, drop), HTTP upload (XEP-0363), OMEMO-encrypted files (aesgcm://), thumbnails before the download, automatic download by policy, the thumbnails drawn kept in the account's store so a chat opened again needs no decode or download, a file loaded by hand drawn from the cache on later visits, retry of a failed transfer. |
🔵 MOSTLY |
| Keep attachments decrypted | A setting, off by default. Off, an encrypted file is kept as ciphertext and decrypted in memory when shown; on, it is decrypted once when it lands and the plaintext is kept, and turning it either way decrypts or reseals what is already stored. | 🔵 MOSTLY |
| Direct file transfer | Jingle (XEP-0234). Device to device over a data channel or in-band, encrypted with JET where the peer offers it; accept, decline, cancel. In the desktop and Android apps only: the browser's backend has no transfer engine. | 🔵 MOSTLY |
| Voice and video messages | Record and play. The composer's record button stands in for Send while the field is empty; a tap switches between voice and a round video message, hold records, release sends, slide up locks, slide away cancels. Voice is played in the bubble, a round video loops muted while in view and plays with sound on a tap; recorded in the format other clients play best. Recording and playback on the Linux desktop fixed for WebKitGTK; video messages not yet tried on a phone. | 🟡 WIP |
| Media viewer | Pictures, video, sound. A picture opens with one tap, our own sent one included; a full-window viewer (zoom and pan, the engine's player, full screen) and the chat's media browser by month; H.264, HEVC and AAC in the AppImage. Save, Share and Open on Android have not been tried on a phone. | 🟡 WIP |
| Calls in a browser | Audio and video, 1:1. The page's own WebRTC under libtacky's Jingle signalling, the DTLS fingerprint sealed with OMEMO and its verdict shown, device pickers (in the call and in Preferences, Calls, with a microphone level and a camera preview to check them), the call minimised to a strip, call history. | 🔵 MOSTLY |
| Calls in the apps | Audio only. The desktop and Android apps use libtacky's native media engine and place audio calls, on the microphone and speaker picked in Preferences, Calls (on Android the call's speaker button routes the sound); Android rings over the lock screen with a call notification, not yet tried on a phone. No video here, no group calls anywhere. | 🟡 WIP |
| Notifications | While the app runs. One notice per chat as Gajim and Dino show them -- the chat's picture, its name and the message, none while the window has the focus -- withdrawn when read anywhere, a click opens the chat: the service worker in a browser, D-Bus on Linux, the system's toasts on Windows and macOS, Android's own; per-chat mute and notification settings. The unread count (every enabled account's, muted chats left out) is in the title, (N) another.im, and on Linux on the launcher too, over the Unity LauncherEntry signal Ubuntu Dock, Dash to Dock, Dash to Panel and KDE show (plain GNOME Shell's dash needs one of those extensions); see docs/push.md. |
🔵 MOSTLY |
| Push | While the app is closed. Web Push in a browser and an installed PWA, UnifiedPush on Linux and Android; a push names who wrote and how many, never what. A muted chat no longer pushes with mod_another_fpush 0.6.0 on the server, and the Android and Linux apps drop a muted chat's push themselves too. Needs another-push-server on the account's server and, outside a browser, a UnifiedPush distributor. | 🟡 WIP |
| Delivery check | Will messages arrive while closed? On the first run and in Preferences: push, a background mode (Android's foreground service, the desktop's hide-on-close and autostart) or only while open, with the fix for the platform. | 🔵 MOSTLY |
| App lock | A PIN over the app. Set, change and remove in Preferences, a back-off after wrong PINs, relock after time away. No biometrics. | 🔵 MOSTLY |
| Encrypted store | The local store's passphrase. Encrypt, decrypt and unlock at start, in the desktop and Android apps; the browser's store has no codec. | 🔵 MOSTLY |
| Certificates | A certificate that does not verify. Trust it, ignore failures for 7 days, or give up; a changed pinned certificate announced. In the apps only: in a browser the browser decides. | 🔵 MOSTLY |
| Backup | Export and import of the whole installation (XEP-0227), plain or sealed with a passphrase. | 🔵 MOSTLY |
| Appearance | Themes, accent and layout. Two themes, Night (the default) and Day -- the chat list in Signal-like white, the chat itself (background, bubbles, composer) in a cool blue -- or the system's light or dark choice (two switches: Follow the system, on by default, and Night mode); an accent colour from eight pastel presets (deepened in Day until they read), the theme's own Signal blue, or Auto, taken from the chat wallpaper's picture (fast-average-color) and adjusted until the words on it read at WCAG AA in either theme; interface scale, chat wallpaper, a resizable list; the system's own face on every platform (the GTK font on Linux, SF on macOS, Segoe UI Variable on Windows, Roboto on Android), with Fira Sans Condensed bundled for the account name in the list header. | 🔵 MOSTLY |
| Translations | Seven languages. English, Estonian, Finnish, Latvian, Lithuanian, Russian and Ukrainian; all 1,174 strings translated in each (make i18n-check). |
🔵 MOSTLY |
| Links | xmpp: URIs. Opening a room or an invitation asks first; one opened before any account exists waits for the first sign-in. |
🔵 MOSTLY |
| System tray | The desktop apps. The app's monochrome glyph with a red dot while anything is unread, Show/Hide, a status menu (online, away, do not disturb, offline) and Quit (Ctrl+Q); "Show icon in the tray" in Preferences, Chats and appearance, on by default wherever something can show the icon (on Linux a StatusNotifierItem host: KDE, or GNOME with the AppIndicator extension) and hidden where nothing can; "Stay connected when closed" (Notifications) hides the window to the tray on close only while the icon is shown, and otherwise closing the window quits the app. Not yet tried on Windows or macOS. | 🟡 WIP |
| Own title bar | The Linux desktop app (the Flatpak too). The header bars are the title bar: their empty space moves the window, a double-click maximises (or does what the desktop says), the window buttons are drawn at their ends in the desktop's layout (GNOME's button-layout, read through the XDG Settings portal), and invisible edges resize it. While it floats it is drawn as a libadwaita window, as Nautilus is: 12px rounded corners, a soft shadow and a hairline edge, the window transparent round them and a click on the shadow passing through to what is behind; square and edge to edge maximised, full screen or tiled (the tiled state read from GTK), and square without a compositor. Close goes the window manager's way, so "Stay connected when closed" still applies. "Use the system title bar" in Preferences, Chats and appearance, brings the frame back at once. On macOS the overlay title bar: the system's traffic lights float over the header bars, which leave them room (none in full screen), drag the window and zoom or minimise on a double-click as System Settings says; the same setting gives the standard title bar from the next start. Not yet tried on a Mac. Windows keeps its own frame. |
🟡 WIP |
| Diagnostics | Logs. The log level, a log file in the apps, the log view. | 🔵 MOSTLY |
| Connection info and XML console | From the account menu. Connection info: the transport, endpoint and how it was found, TLS, SASL, SASL2, FAST, Bind 2, stream management and CSI, the server's software and identities, libtacky's checklist of XEPs and RFCs the server supports, HTTP upload and its limit, push, the contact addresses, our caps and the server's features, all copyable. XML console: the account's traffic one tag a line, filter, direction, pause, with bytes and stanzas counted each way, in a window of its own beside the chat (a second native window on the desktop, a popup in a browser, the sheet when the popup is blocked and on Android). | 🟡 WIP |
| WebXDC, channels, group calls | On the experimental branch; not in this release. |
🔴 TODO |
| Stickers | XEP-0449. Sticker packs and sending stickers: not started (libtacky's XEP-0449 row is TODO too); a received sticker shows as the picture it shares. | 🔴 TODO |
Status and tests
Alpha (0.4.7): breaking changes may still be made. Every feature above has tests against a fake transport or the in-page mock backend, and the shell's plain-std modules have unit tests of their own. Nothing has been verified by hand against public servers and other clients as a release check, which is why no row above is DONE.
51,564 lines of code and 33,894 lines of tests (the table under
Architecture). Coverage: 90.3% of the UI's lines, 95.6% of the shell's
plain-std modules' (make coverage) and 86.9% of the worker's web/; the
Kotlin and the binding not measured. The test cases the tree defines:
| suite | how it runs | files | cases | last run |
|---|---|---|---|---|
| UI (vitest, happy-dom) | make test-ui |
145 | 1,890 | 2026-10-12, all passed |
| Types and language catalogues | make check (tsc, then make i18n-check) |
-- | -- | 2026-10-12, passed |
| Tauri shell, plain-std modules | make test-shell |
19 | 113 | 2026-10-12, all passed |
Tauri shell, the whole crate against libtacky.a |
make test-embed, after make backend |
22 | 107 | 2026-10-10 against libtacky v1.0.0-alpha11, all passed (80 built without the desktop feature: 74 in the crate, 6 in tests/embed.rs) |
| Android, JVM unit tests | gradle :app:testUniversalFreeReleaseUnitTest and :app:testUniversalGmsReleaseUnitTest in the android image (the same tests in each flavor) |
19 | 173 | 2026-10-12, all passed in both |
| wasm worker, under node | make test-worker |
5 | 160 checks | 2026-10-12 against libtacky v1.0.0-beta7: boot 35, omemo 3, host 18, store 64 and page 40 passed |
| WebKitGTK layout smoke | make webkit-smoke |
1 | 1,048 checks | 2026-10-10, all passed |
Chromium smoke of dist/web |
make web-smoke |
1 | 1 | not run for this count |
How to use it
Everything builds and tests in this repository's container images; the
machine needs only git and podman (or docker). docker/run.sh builds the
image a target needs, tags it anotherim-js-build:<profile> and runs the
target with this checkout mounted at /src.
git clone --recurse-submodules https://dev.another.im/another-im/anotherim-js.git
cd anotherim-js
make docker-build # the backend, dist/web, the web tarball, the AppImage
make test-ui check test-shell # the tests that need no backend
Every make target runs in the image: on the host, make <goals> only
starts the container (docker/run.sh make <goals>, the command line's
variables with it), in the android or windows image for those targets. The
docker-*, bundle-* and web-image/web-up/web-down targets are
not wrapped again: each is already one container launch, all its work in
the image. Only help runs on the host. On macOS, which has no build image,
every target runs on the Mac instead (see docs/macos.md). The submodules are
fetched in the image too, by the targets that need them and only when
missing, over public https: the backend from the forge, its third-party
pins from GitHub. To push to libtacky over ssh from
such a clone, set once
git config --global url.ssh://git@dev.another.im:3022/.pushInsteadOf https://dev.another.im/. make dev and make serve are published on
the host's loopback; make app opens its window on the host's display,
keeping its data under build/app-home. HOST=1 runs a target on the host
anyway, for a machine set up to build without containers.
| platform | target | output | status |
|---|---|---|---|
| Browser | make docker-build (in the image: make web-tar) |
dist/web/ and dist/anotherim-js-<version>.tar.gz, a static tree any server can host |
🔵 MOSTLY |
| Linux AppImage | make docker-build or make bundle-appimage |
dist/another-im-tauri-<version>-x86_64.AppImage |
🔵 MOSTLY |
| Linux Flatpak | make bundle-flatpak (the flatpak image, run privileged; the GNOME 50 runtime) |
dist/another-im-tauri-<version>-x86_64.flatpak |
🟡 WIP |
| Windows | make docker-windows (with a smoke run under wine) or make bundle-windows |
dist/another-im-tauri-<version>-x86_64-setup.exe (NSIS) |
🟡 WIP: cross-built and run under wine, never on Windows |
| Android | make docker-android or make bundle-android |
dist/another-im-tauri-<version>[-unsigned].apk (arm64-v8a, Android 11 and later), the free build: UnifiedPush, no Google code |
🟡 WIP: built, parts not yet tried on a phone |
| Android with FCM | make bundle-android-gms ANOTHER_IM_GOOGLE_SERVICES=<google-services.json> |
dist/another-im-tauri-<version>-gms[-unsigned].apk, the gms build: FCM where no UnifiedPush distributor is installed (docs/push.md) |
🟡 WIP: built, FCM not yet tried on a phone |
| macOS | make bundle-macos, on a Mac only, see docs/macos.md |
src-tauri/target/aarch64-apple-darwin/release/bundle/: .app and .dmg (unsigned) |
🟡 WIP: built on Apple silicon, the app starts; sign-in and calls not yet tried |
The apk is signed when ANOTHER_QT_ANDROID_KEYSTORE,
ANOTHER_QT_ANDROID_KEY_ALIAS and ANOTHER_QT_ANDROID_KEYSTORE_PASS are set,
unsigned otherwise; ANOTHER_IM_ANDROID_DEBUG=1 makes gradle's debug apk.
The app's ID is im.another.messenger on every platform; up to 0.4.6 it
was im.another.desktop. On the desktop nothing is lost: the first start
of a newer build moves the old ID's directories to the new names before it
opens anything (src-tauri/src/id_migrate.rs: the XDG data, config and
cache directories and the autostart entry on Linux, the whole old sandbox
in the Flatpak, %APPDATA% and %LOCALAPPDATA% on Windows, Application
Support, Caches and WebKit on macOS), and a directory already there under
the new name is left as it is. The Flatpak installs as a new app beside
the old one, which is then empty and can be uninstalled. On Android the ID is the package name, and
Android has no way to rename a package: the new apk is a new app that
does not upgrade the old one. Testers install it beside the old app, log
in again (the old app's store, accounts and keys stay in the old app), and
uninstall the old one when they no longer need it; adb commands name the
new package (adb logcat --pid=$(adb shell pidof im.another.messenger)).
JOBS=<n> caps the parallelism. make docker-shell (PROFILE=android or
PROFILE=windows for those images) is a shell where each target below runs
as it is:
| target | what it does |
|---|---|
make, make web |
the browser client, dist/web (the wasm backend and the UI) |
make web-tar |
dist/web as a tarball |
make backend |
libtacky.a, the libwebrtc media backend and the wasm backend, through libtacky's own make, every tree under build/backend |
make test |
test-ui, test-shell, test-worker, and test-embed when libtacky.a is built |
make check |
tsc --noEmit, then make i18n-check (STRICT=1: a missing translation fails too) |
make webkit-smoke |
ui/dist drawn in WebKitGTK, the desktop's engine, screen by screen (SMOKE_LANG=uk: in a language pack) |
make web-smoke |
dist/web booted in headless Chromium (CHROME=) |
make dev |
local web development on http://localhost:5173: the UI with the real wasm backend, the UI hot-reloaded and the worker rebuilt and the page reloaded when the backend's sources change (DEV_BACKEND=mock: the in-page mock, nothing built; DEV_POLL=1 where file events do not cross the mount) |
make web-serve |
dist/web on http://localhost:8000 |
make app |
the desktop window, debug, after make backend |
make help |
every target |
Two build knobs shape the client: AUTH_SOURCES (the welcome screen's ways
in, xmpp and sso, both by default; SSO_BROKER_URL names the broker,
without which the organisation button is hidden)
and MODULES (the optional areas calls, transports, threads,
backup and lock, all by default). A module left
out is neither registered nor bundled.
On the wire the client is another.im (Js), its XEP-0030 identity web,
pc or phone. The apps keep their profile under
~/.config/libtacky/another-im-js and its XDG siblings (the platform's
equivalents on Windows and macOS, the app's private storage on Android), the
browser in its Origin Private File System, files used in place (IndexedDB in
a browser without OPFS sync access handles; a store kept there by an earlier
build is moved into OPFS once). Two apps cannot hold one profile: the second
refuses to start and names the first, as a second browser tab does. --config-dir, --data-dir, --cache-dir,
--debug-level and --debug-file are passed on to the backend, and
ANOTHERIM_DEBUG_LEVEL and ANOTHERIM_DEBUG_FILE stand in for the last two
where there is no command line. In the apps every HTTP request (uploads,
downloads, the SSO token) is libtacky's own; in a browser tab it is the
browser's.
Serving the browser client (web.another.im)
The browser client is a static tree. On the server (git and podman or docker are all it needs):
git clone --recurse-submodules https://dev.another.im/another-im/anotherim-js.git
cd anotherim-js
make web-up # builds dist/web in the build image, wraps it in nginx, runs it on 127.0.0.1:8080 (WEB_BIND, WEB_PORT)
make web-up builds the image anotherim-js-web (docker/web-image.Dockerfile:
nginx with dist/web, the Content-Security-Policy and cache headers the client
needs, /healthz) and (re)starts the container anotherim-js on
127.0.0.1:8080 (WEB_PORT= for another), restarting unless stopped. The
front proxy on the machine terminates TLS and forwards the public name to it,
for nginx:
server {
listen 443 ssl; listen [::]:443 ssl; http2 on;
server_name web.another.im;
# ssl_certificate ... / ssl_certificate_key ...
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}
It must be https: a browser allows the local store, the service worker and
the camera and microphone on a secure origin only. To update: git pull && git submodule update --init --recursive && make web-up. make web-down
stops it. To have systemd run it instead (started at boot, restarted when
it stops), make systemd-unit prints the unit for the engine in use: a
Quadlet .container for podman, a .service running the container in the
foreground for docker, with the engine's path and the WEB_* variables
(WEB_NAME, WEB_IMAGE, WEB_BIND, WEB_PORT) filled in:
sudo loginctl enable-linger $USER # once, rootless: the user's services start at boot
make web-image # the image the unit runs
# podman:
mkdir -p ~/.config/containers/systemd
make systemd-unit > ~/.config/containers/systemd/anotherim-js.container
# docker (rootless; /etc/systemd/system/ for the system daemon):
mkdir -p ~/.config/systemd/user
make systemd-unit > ~/.config/systemd/user/anotherim-js.service
systemctl --user daemon-reload && systemctl --user restart anotherim-js
After a git pull, make web-image and the restart again;
systemctl --user status anotherim-js and journalctl --user -u anotherim-js show it. As root the unit goes in /etc/containers/systemd/
(podman) or /etc/systemd/system/ (docker) and systemctl runs without
--user. The client finds the XMPP WebSocket through the domain's host-meta,
else at wss://<domain>/xmpp-websocket.
make serve is the quick look instead: the same tree from a python server on
127.0.0.1:8000, in the foreground.
The backend
libtacky is the submodule libtacky/
(https://dev.another.im/another-im/libtacky.git, given relative in
.gitmodules so it follows however this repository was cloned), pinned by this
repository and tracking libtacky's develop branch; a recursive clone brings
its third-party submodules too. The backend is looked for in this order:
TACKY_ROOT=<dir> (or TACKY=<dir> to make), ../libtacky beside this
checkout, ../another-tacky (its old name), then the nested libtacky/. To move
the pin to the tip of develop:
git submodule update --remote libtacky && git add libtacky
A clone made while the submodule was still at tacky/ (before 2026-10-02)
moves over with:
git pull
rm -rf tacky .git/modules/tacky
git submodule sync --recursive && git submodule update --init --recursive
Licence
anotherim-js is MPL-2.0 (LICENSE). libtacky, which every build embeds
(the wasm worker, the Tauri shells), is GPL-3.0, so a build is distributed
under the GPL-3.0 terms as a whole; MPL-2.0 is compatible with that. The
bundled typeface, Fira Sans Condensed (ui/public/fonts/), is © 2012-2015
the Mozilla Foundation and Telefonica S.A. under the SIL Open Font License
1.1 (ui/public/fonts/OFL.txt). The icons are Lucide's (https://lucide.dev),
© Lucide Icons and Contributors under the ISC licence, some derived from
Feather (© Cole Bemis, MIT); both texts ship as ui/public/licenses/lucide.txt,
and tools/icons-from-lucide.mjs regenerates ui/src/views/icons.ts from the
pinned release. The AppImage carries FFmpeg's decoders
(LGPL-2.1+) for H.264, HEVC and AAC, which are subject to patents in some
jurisdictions.

