Desktop game-session monitor for Questory.
Watches what’s running, figures out which game it is,
stores sessions locally, and pushes completed playtime to your Questory instance.
- 🎮 Steam-first identity — AppID ground truth via launch reaper + local library index.
- 🔎 Discord detectable catalog — cached locally, refreshed daily (URL overridable in Settings).
- 📚 Local catalog + confirm — custom titles and user-confirmed non-Steam games.
- 📤 Reliable outbox — embedded Turso DB with retry, ack, and retention purge (7 or 30 days).
- 🔐 Device login — auth code + PKCE, device-bound refresh to your Questory instance.
- 🖥️ Tray-friendly UI — compact Home / Games / Settings; optional minimize/close-to-tray and start-at-login.
qMonitor sits in the background (or the tray), polls running processes, and resolves each match through a Steam-first identity pipeline:
- Steam AppID — launch reaper + local library index when Steam is the source of truth
- Discord detectable catalog — cached locally, refreshed daily (URL overridable)
- Local catalog + confirm — custom titles and user-confirmed non-Steam games
Completed sessions land in a local outbox DB, then sync to Questory via webhook with retry, ack, and retention purge.
flowchart LR
P[Process poll] --> I[Identity resolve]
I --> S[Session store<br/>Turso outbox]
S --> W[Webhook push]
W --> Q[Questory API]
W -->|2xx ack| S
W -->|401| R[Token refresh]
R --> W
| Domain | Technologies |
|---|---|
| UI | Tauri 2 + React (Home / Games / Settings) |
| Core | Rust — process detect, identity, auth, push, DB |
| Data | Embedded Turso outbox |
| Auth | Device login (PKCE) + OS keyring for tokens |
| Platforms | Windows (NSIS + MSI) · Linux (AppImage + .deb + Arch) |
- Node.js 20+ and pnpm
- Rust stable toolchain
- Tauri 2 prerequisites for your OS
pnpm install
pnpm tauri devUseful aliases: pnpm tauri:dev · frontend-only Vite: pnpm dev (port 1420).
pnpm tauri buildBundles (see src-tauri/tauri.conf.json):
| Platform | Artifacts |
|---|---|
| Windows | NSIS (*-setup.exe) + MSI (currentUser install) |
| Linux | AppImage + .deb |
| Arch | .pkg.tar.zst (CI repackages the .deb; see packaging/arch/) |
GitHub Actions publishes installers automatically:
| Channel | Trigger | Tag / name |
|---|---|---|
| Stable | Merge / push to release |
Immutable v{version} from package.json only — bump that file; Cargo.toml / tauri.conf.json sync at build. GitHub Latest is a badge, not a reused tag. |
| Canary | Merge / push to main |
Immutable prerelease v{version}-canary.{shortsha} (prerelease, not Latest). Previous canaries stay on Releases. |
Download from the repo Releases page. In the app, Settings → Updates checks that channel once per day and offers a link to the matching tag (no auto-install). Arch example:
sudo pacman -U qmonitor-*.pkg.tar.zst💡 Tip: PRs run CI (frontend build +
cargo test) only; they do not publish artifacts.
- Open Settings and set your Questory base URL (web origin like
https://app…or API origin likehttps://api…). - Save — qMonitor probes
{baseUrl}/api/health, detectsfevsbe, and storesapiRoot/webOrigin. - Sign in with device login (browser consent + loopback callback).
- Optionally point at a local game catalog and tweak tray / retention prefs.
Config lives under the OS config dir:
| OS | Path |
|---|---|
| Windows | %APPDATA%\qMonitor |
| Linux | ~/.config/qMonitor |
Tokens use the OS keyring (access_token + refresh as session_token). Device id is derived from a hashed install salt (device_salt in the config dir). Settings → Dev token is a local override for webhook pushes only.
See config.example.json for the full shape:
| Key | Default | Notes |
|---|---|---|
baseUrl |
— | Questory web or API origin |
pollIntervalSecs |
3 |
Process poll cadence |
retentionAckedDays |
30 |
Synced-row purge (7 or 30) |
catalogPath |
— | Path to local catalog JSON |
detectableUrl |
Discord v10 detectable | Cached as detectable.json |
steamPathOverride |
— | Non-default Steam install |
dbPath |
qmonitor.db in config dir |
Local Turso file |
startAtLogin / minimizeToTray / closeToTray |
false |
Autostart & tray behavior |
For titles that Steam / Discord don’t pick up, use a JSON catalog (example: catalogs/games.example.json):
[
{
"id": "local:hades",
"name": "Hades",
"executables": [
{ "os": "windows", "name": "Hades.exe", "is_launcher": false },
{ "os": "linux", "name": "Hades", "is_launcher": false }
],
"path_hints": ["Hades"],
"arguments": null
}
]Point catalogPath at that file (or add titles from the Games UI).
qMonitor/
├── src/ # React UI (Home / Games / Settings)
├── src-tauri/ # Rust core: detect, identity, auth, push, DB
├── packaging/arch/ # PKGBUILD for Arch (.pkg.tar.zst / future AUR)
├── catalogs/ # Example local game catalog
└── config.example.json # Config shape reference
Click to view webhook payload and OAuth paths
POST {apiRoot}/webhooks/qmonitor
Authorization: Bearer <accessToken>
{
"schema_version": 1,
"session_id": "uuid",
"source": "steam",
"steam_app_id": 570,
"title": "Dota 2",
"exe": "dota2.exe",
"started_at": "...",
"ended_at": "...",
"duration_secs": 4500,
"host": { "os": "windows", "hostname": "..." }
}- HTTP 2xx → session acked in the outbox
- 401 → refresh with
device_id+ refresh token, then retry
OAuth paths (relative to the resolved API/web origins): /oauth/qmonitor/authorize, /oauth/qmonitor/token, /oauth/qmonitor/revoke.
This project is distributed under the MIT License. See LICENSE for details.
MIT © Questory Labs