Quple
A tiny "call someone by username" desktop app built around Jitsi Meet. Modeled on quintodrome, it wraps a web app in a Tauri shell — but instead of hosting a local sidecar server, it points at a hosted (or self-hosted) Jitsi Meet deployment and uses a minimal central service to let two users find each other and agree on a meeting room.
How it works
- Sign in. Everything is behind a Quine account at the one central
identity server,
identity.quineglobal.com(an OpenID Connect provider inidentity/). The app opens the sign-in page in the system browser. Your Quine username is what people call you by. Contacts are kept per person on the signaling server. - Presence. While the app is open, it polls the signaling server every few seconds, which doubles as "I'm online" and "did anyone call me?".
- Calling. You call someone by username, on whichever Jitsi Meet
server you like: meet.jit.si by default, or any other deployment (the
app remembers the last one you used). Jitsi servers need no credentials
from Quple, so nothing is set up on them. The signaling server mints a
meeting ID and your app opens a window at
<jitsi>/<meeting-id>. - Receiving. The callee's app sees the pending invite on its next poll, shows an "Incoming call from X on " prompt, and — if accepted — opens the same meeting URL on the caller's chosen server. Jitsi Meet's own WebRTC signaling (XMPP + the videobridge) takes it from there; Quple's server never touches media or call content, it only introduces two clients to the same room name.
Every request to the signaling server carries a Quine access token; the server takes the caller's username from it, never from the request.
meet.jit.si only starts a meeting once a signed-in user joins, so when the chosen server is meet.jit.si the app offers to sign you in to it (once; it's remembered).
Caller's Quple Signaling server Callee's Quple-------------- ---------------- --------------POST /call -----> mint meetingId,{to, jitsiUrl}queue invite for "to"<----- {meetingId, jitsiUrl}open window:<jitsiUrl>/<id>GET /poll<----- (polling every ~3s)return queued invite ----->show "Incoming callfrom X on <server>"accept -> open window:<jitsiUrl>/<id>
Both windows are now just two independent visitors to the same Jitsi Meet
room — the same as if they'd both manually opened
https://meet.jit.si/quple-abc123 and shared the link.
Layout
quple/├── identity/ # Go: Quine sign-in (identity.quineglobal.com) - an OIDC│ # provider on zitadel/oidc, with GitHub/Google via goth├── server/ # Go: the minimal central signaling service├── desktop/ # Tauri desktop app (the thing each user runs); the UI is│ # Preact + htm, vendored, so there's no frontend build└── jitsi-meet/ # Upstream jitsi-meet source, as its own git clone (ignored# here; reference only, not built as part of this app —# see below)
Why jitsi-meet/ isn't built
Quple doesn't build or serve jitsi-meet's frontend itself. It just navigates
a window to a room URL on some Jitsi Meet deployment, the same way a
browser tab would. By default that's the public meet.jit.si, which requires
no infrastructure at all. Running your own deployment needs Prosody (XMPP),
Jicofo, and JVB in addition to the web frontend in this repo — that's a
separate, heavier project (see jitsi-meet's own doc/quick-install.md and
docker-compose setup). The source is vendored here so it's easy to build a
custom/branded frontend later; once one is running, just type its URL as the
server when calling (or make it the signaling server's DEFAULT_JITSI).
Running it
Requires Go 1.25+.
1. Identity server (Quine sign-in)
cd identitygo run . # serves issuer https://identity.quineglobal.com on :8081
Environment: ISSUER (the public base URL, exactly as clients see it),
ADDR, DATA_DIR (default ./data: the signing key, accounts.json, and
hashed refresh tokens - back it up and keep it private).
GITHUB_CLIENT_ID/GITHUB_CLIENT_SECRET and
GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET turn on "Continue with GitHub/Google";
register $ISSUER/login/github/callback and $ISSUER/login/google/callback as
the OAuth apps' callback URLs.
Accounts are invite-only, managed with the same binary (it re-reads
accounts.json, so this works while the server runs):
go run . adduser alice # prompts for a password (empty = social sign-in only)go run . link alice github octocat # this GitHub user may sign in as alicego run . link alice google a@b.com # this (verified) Google address may sign in as alicego run . listgo run . passwd alice / unlink alice github / deluser alice
A social link is pinned to the provider's stable user ID on first use, so a later rename or recycled login can't take the account over. Deleting an account signs it out everywhere within an hour (refresh is refused).
It runs behind TLS termination in production; plain http:// issuers are
allowed for local testing only.
2. Signaling server
cd servergo run . # listens on :8080; set ADDR to change it
ISSUER (default https://identity.quineglobal.com) is the identity server
whose access tokens it accepts and CLIENT_ID (default quple) the app they
must be issued to. Every endpoint needs Authorization: Bearer <access token>.
DEFAULT_JITSI (default https://meet.jit.si) hosts calls that don't name a
server, and DATA_DIR holds contacts.
Deploy this wherever meet.quineglobal.com (or whatever host name you pick)
resolves to — it's stateless-enough to run as a single small process; restart
just clears presence and any in-flight invites (contacts are kept in
DATA_DIR).
Local end-to-end setup
(cd identity && ISSUER=http://127.0.0.1:8081 ADDR=127.0.0.1:8081 go run . &)(cd server && ISSUER=http://127.0.0.1:8081 ADDR=127.0.0.1:8090 go run . &)(cd identity && go run . adduser alice)(cd identity && go run . adduser joe)
Then in the app's Advanced settings set the identity server to
http://127.0.0.1:8081 and the signaling server to http://127.0.0.1:8090.
3. Desktop app
Requires a Rust toolchain (pinned via desktop/rust-toolchain.toml,
installed automatically by rustup) and, on Linux, the system WebView
packages Tauri needs (webkit2gtk-4.1, gtk3, libappindicator3,
librsvg2 — see Tauri's Linux prerequisites).
On this machine those aren't installed at the OS level (it's an immutable/
atomic distro), so build it the same way quintodrome does for Linux: inside
the flatpak-builder distrobox, or via rpm-ostree install + reboot if you
want it on the host directly. macOS and Windows need no extra system
packages beyond Xcode CLT / the MSVC Build Tools.
cd desktop/src-tauricargo run
On first launch you'll land on Sign in with Quine, which finishes in your
browser and comes back to the app (RFC 8252: a one-shot listener on
127.0.0.1). The refresh token is kept in the app's data dir, readable only
by you; the webview only ever sees short-lived access tokens. The identity
and signaling server URLs are under Advanced settings, and later the gear
icon. The Jitsi server for a call is picked on the main screen.
To build a standalone app bundle:
cd desktop/src-tauricargo tauri build
Verification status
identity/:go vet-clean, with end-to-end tests over real HTTP (go test ./...): password sign-in through the zitadel provider, PKCE and single-use codes, refresh-token rotation and revocation, refusal after an account is deleted, unregistered redirect URIs refused, social links pinned to the provider's user ID, and tokens verified withcoreos/go-oidcthe way the signaling server does it. The GitHub/Google round trip itself (goth) has not been exercised: it needs real OAuth app credentials.server/:go vet-clean, with HTTP-level tests for auth, calls on the default and a caller-chosen Jitsi server (and refusal of non-http(s) or otherwise odd server URLs), and contacts.desktop/: builds withcargo buildon macOS and runs. The Preact UI (Quine sign-in, contacts, picking the server for a call, the meet.jit.si sign-in prompt, incoming calls showing their server) is checked headlessly against a mocked backend. The browser sign-in round trip in the real app hasn't been driven automatically.
Server API
All endpoints require Authorization: Bearer <Quine access token>; the
caller's username is the token's subject.
| Method & path | Body | Returns |
|---|---|---|
GET /me |
— | {"username":"alice","defaultJitsiUrl":"https://meet.jit.si"} |
POST /register |
— | {"ok":true} — marks you online |
POST /call |
{"to":"bob","jitsiUrl":"https://meet.jit.si"} |
{"meetingId":…,"jitsiUrl":…} — jitsiUrl is optional (default DEFAULT_JITSI) |
GET /poll |
— | {"invites":[{"from":…,"meetingId":…,"jitsiUrl":…,"createdAt":…}]} — also marks you online, and clears delivered invites |
GET /contacts |
— | {"contacts":[{"username":…,"online":true}]} |
POST /contacts |
{"username":"bob"} |
the added contact |
DELETE /contacts/{username} |
— | {"ok":true} |
Presence expires 90s after the last /register or /poll. Undelivered
invites expire after 2 minutes.