# QupleA tiny "call someone by username" desktop app built around[Jitsi Meet](https://jitsi.org). Modeled on [quintodrome](../quintodrome), itwraps a web app in a [Tauri](https://tauri.app) shell — but instead of hostinga local sidecar server, it points at a hosted (or self-hosted) Jitsi Meetdeployment and uses a minimal central service to let two users find eachother and agree on a meeting room.## How it works1. **Sign in.** Everything is behind a Quine account at the one centralidentity server, `identity.quineglobal.com` (an OpenID Connect provider in`identity/`). The app opens the sign-in page in the system browser. YourQuine username is what people call you by. **Contacts** are kept perperson on the signaling server.2. **Presence.** While the app is open, it polls the signaling server everyfew seconds, which doubles as "I'm online" and "did anyone call me?".3. **Calling.** You call someone by username, on whichever Jitsi Meetserver you like: meet.jit.si by default, or any other deployment (theapp remembers the last one you used). Jitsi servers need no credentialsfrom Quple, so nothing is set up on them. The signaling server mints ameeting ID and your app opens a window at `<jitsi>/<meeting-id>`.4. **Receiving.** The callee's app sees the pending invite on its next poll,shows an "Incoming call from X on <server>" prompt, and — if accepted —opens the same meeting URL on the caller's chosen server. Jitsi Meet'sown WebRTC signaling (XMPP + the videobridge) takes it from there;Quple's server never touches media or call content, it only introducestwo clients to the same room name.Every request to the signaling server carries a Quine access token; theserver 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 thechosen server is meet.jit.si the app offers to sign you in to it (once; it'sremembered).```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 Meetroom — 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 builtQuple doesn't build or serve jitsi-meet's frontend itself. It just navigatesa window to a room URL on *some* Jitsi Meet deployment, the same way abrowser tab would. By default that's the public `meet.jit.si`, which requiresno infrastructure at all. Running your own deployment needs Prosody (XMPP),Jicofo, and JVB in addition to the web frontend in this repo — that's aseparate, 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 acustom/branded frontend later; once one is running, just type its URL as theserver when calling (or make it the signaling server's `DEFAULT_JITSI`).## Running itRequires Go 1.25+.### 1. Identity server (Quine sign-in)```shcd 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`, andhashed 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` asthe 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):```shgo 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 alater rename or recycled login can't take the account over. Deleting anaccount signs it out everywhere within an hour (refresh is refused).It runs behind TLS termination in production; plain `http://` issuers areallowed for local testing only.### 2. Signaling server```shcd servergo run . # listens on :8080; set ADDR to change it````ISSUER` (default `https://identity.quineglobal.com`) is the identity serverwhose access tokens it accepts and `CLIENT_ID` (default `quple`) the app theymust be issued to. Every endpoint needs `Authorization: Bearer <access token>`.`DEFAULT_JITSI` (default `https://meet.jit.si`) hosts calls that don't name aserver, 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; restartjust clears presence and any in-flight invites (contacts are kept in`DATA_DIR`).### Local end-to-end setup```sh(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 appRequires a Rust toolchain (pinned via `desktop/rust-toolchain.toml`,installed automatically by `rustup`) and, on Linux, the system WebViewpackages Tauri needs (`webkit2gtk-4.1`, `gtk3`, `libappindicator3`,`librsvg2` — see [Tauri's Linux prerequisites](https://v2.tauri.app/start/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: insidethe `flatpak-builder` distrobox, or via `rpm-ostree install` + reboot if youwant it on the host directly. macOS and Windows need no extra systempackages beyond Xcode CLT / the MSVC Build Tools.```shcd desktop/src-tauricargo run```On first launch you'll land on **Sign in with Quine**, which finishes in yourbrowser 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 onlyby you; the webview only ever sees short-lived access tokens. The identityand signaling server URLs are under **Advanced settings**, and later the gearicon. The Jitsi server for a call is picked on the main screen.To build a standalone app bundle:```shcd 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 andsingle-use codes, refresh-token rotation and revocation, refusal after anaccount is deleted, unregistered redirect URIs refused, social links pinnedto the provider's user ID, and tokens verified with `coreos/go-oidc` theway 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 onthe default and a caller-chosen Jitsi server (and refusal of non-http(s)or otherwise odd server URLs), and contacts.- **`desktop/`**: builds with `cargo build` on macOS and runs. The Preact UI(Quine sign-in, contacts, picking the server for a call, the meet.jit.sisign-in prompt, incoming calls showing their server) is checked headlesslyagainst a mocked backend. The browser sign-in round trip in the real apphasn't been driven automatically.## Server APIAll endpoints require `Authorization: Bearer <Quine access token>`; thecaller'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`. Undeliveredinvites expire after 2 minutes.