Skip to content

Sites: secure mailboxes, chats, a dating site

Some mail never leaves its website: a bank's secure mailbox, the hospital's, the tax office's. Chats live in the browser too (Element, WhatsApp, Discord), and so does a dating site. Sites (Ctrl+4) keeps them pinned, logged in once, and turns their notifications into something that waits for you.

What a site is

Not an account: it holds no sign-in of Sioul's (the site keeps its own, in its profile). [[site]] in the configuration; files older than 4 October 2026 kept sites as [[account]] of kind "portal", read as sites and moved to [[site]] the first time Sioul starts (config::migrate_sites, a copy of the file kept as config-before-sites.toml):

[[site]]
id = "element"
name = "Element"
url = "https://app.element.io"
site = "chat"            # its type, how it behaves: "mailbox" (the default: a secure mailbox, a client area), "chat", "video", "social", "dating", "other"
categories = ["Amis"]    # your own categories, any words ("Banque", "Santé"): the list filters by them
area = "leisure"         # what it is for: "work", "admin", "leisure", joined by "+"; unsaid, by its type (docs/areas.md)
microphone = true        # for calls; unsaid, chats, video calls and dating sites have them, no other site
camera = true
screen = true            # sharing the screen
realtime = false         # its notifications at once
muted = false            # its sounds silenced
background = true        # kept open to hear from it: chats by default
announced_by = []        # the domains whose mail announces it; the site's own domain by default

The list

  • Your order, kept in the configuration: a site's ⋮ ▸ Move up, Move down. The sort button above the list groups them by type instead (secure mailboxes and client areas, chats, video calls, social networks, dating, other), your order kept within each.
  • Filtered (the funnel above the list): by what a site is for (work, your admin, leisure), by its type, by one of your categories, each with "any"; chosen again, a choice is undone. The filters in use are said under the title, with one button to show every site again.
  • Its own icon: asked of the site itself, never of an icon service that would learn which sites you keep (sioul_sync::favicon: the icon its page names, the larger the better, else /favicon.ico), kept in ~/.cache/sioul/favicons/ and asked again after a week; its type's icon until then, and for a site that gives none.
  • The sites for these hours first; the others fold under one line, "Other hours: 3" (areas.md).
  • Changed in place, from its menu: ⋮ for the site open, or a right click on any site of the list: name, address (https only), type and categories; its kind of thing; what it is for (work, admin, leisure, any of them together); the microphone, the camera, sharing the screen; the devices of calls; silenced, kept open; its place in your order; a link; Remove takes it out of Sioul (its sign-in stays in the profile until cleared). Sites are made, changed and removed here only, never in Accounts.
  • Tied to other things: ⋮ ▸ Link to… ties a site to a task, a note, a contact, a project, as a correspondent is (sioul:site/<id>, kept by the other thing, or in links.toml); a link to it opens the site. The link picker finds sites by name and address.

Presets

"Add a site" offers the sites people usually keep (about 400 on 4 October 2026), by country and, where it matters, by state or province (Québec in Canada; American utilities by state), and for everyone: offices (taxes, health insurance, family allowances, jobs, retirement, studies, papers and residence), banks, electricity and gas, phone and internet, chats, video calls, social networks (LinkedIn for work; Facebook, Instagram, Reddit, X, Bluesky, Mastodon… for leisure; Nextdoor for both), dating and friendship. Filtered by type or category, and found by a word ("ameli", "banque"); one click fills the name, the sign-in address, its type, its categories in your language, what it is for, and the domains whose mail announces it; or the fields are filled by hand. Your country is your language's (French in Canada shows Canada), and can be changed. Left out on purpose: services without a web version (Signal, Viber, Session, Hinge, Bumble now), retired ones (Skype).

"Usual sites ▾", beside "Pin a site", opens them as a menu: each country, then its groups (offices, banks, energy, phone and internet; chats, video calls, social networks, dating for everyone), then the sites; a country's states or provinces in a menu of their own ("By state", "By province"), each with its groups, or its sites at once when it has one group. Each country, state and group opens with "All of them (N)", which pins all of that group not pinned yet at once (a country's own, not its states'); a site already pinned is ticked and left as it is. Clicking a group's name opens it rather than pinning it all, so that nothing is pinned by a slip. Sites already pinned are known by their address (host and path), so two services on one host (canada.ca's accounts, a network and its messages) stay two.

The list is a plain JSON file anyone can update without building Sioul: presets/sites.json in the sources, { "tags": { "health": { "name": { "en": "Health", "fr": "Santé" } } }, "countries": { "CA": { "sites": […], "regions": { "QC": { "sites": […] } } } }, "international": […] }, each site { "name", "url", "type", "tags", "area", "announced_by", "about", "calls" }; a country may say what its regions are called in a menu ("regions_name": { "en": "By state", "fr": "Par État" }). Of the copy built in, one in $XDG_DATA_DIRS/sioul/presets/sites.json and yours in ~/.local/share/sioul/presets/sites.json, the one with the latest checked date is used. tools/check-presets.py asks every address as a browser would and says, for each: ok; moved (it answers from another address: worth updating); blocked (it refuses robots: check by hand); broken (not found, no such host: fix it). It exits with 1 when one is broken, so it can run on a schedule; --only FR, --json.

The browser

  • Qt WebEngine (Chromium), started before the application (cpp/webengine.cpp).
  • One persistent profile for every site (sioul-sites): cookies kept, so logins last; files cached on disk (512 MB at most).
  • A site's view is made the first time it opens and kept for the session; a chat's at start, to receive its notifications.
  • Permissions: notifications granted to every site before it asks, and kept (persistentPermissionsPolicy: StoreOnDisk, profile.queryPermission(…).grant()): Sioul catches them and gathers them (below), so no site asks for them again (Proton did, at each visit, while nothing was kept). The microphone, the camera and sharing the screen (you choose a screen or a window, or nothing) as each site's switches say: on for chats, video calls and dating sites, off for the others until you turn them on; a site asking while off is told in the status line, with where to turn it on; a yes kept from before is forgotten as soon as its switch is off. Geolocation never.
  • The devices of calls (a site's menu ▸ Devices for calls…): the camera, the microphone and the speaker, from the system's own list (Qt Multimedia's: the same on Linux, Windows and macOS), the system's own when unsaid ([calls] in the configuration). A script in every page asks for them by name when a call starts (the browser's device ids are each site's own), unless the page chooses one itself, and sends every sound the page plays to the speaker chosen (setSinkId). Names are known to a page once it was allowed a microphone or a camera.
  • Pop-ups (sign-in windows, call windows) open in a window of their own.
  • Downloads go to your downloads folder.
  • The user agent leaves out Qt's name: some chats refuse browsers they do not know.

Notifications at your pace

Every site's notifications are accepted, then caught by Sioul, never shown by the site itself:

  • from the site in front of you: nothing more;
  • from a site in real time (its checkbox, always in view; or real time for everything): a desktop notification at once;
  • a call ("incoming call", "vous appelle"…): at once, unless the site is silenced; a missed call waits like the rest;
  • else it waits, under " has news" in the Porch, in $XDG_STATE_HOME/sioul/site-notices.toml (a notification told twice is kept once; the newest two hundred). Opening the site clears its news.
  • Gathered: at set times (Settings ▸ Reminders, 9:00, 13:00 and 18:00 unless you set others; three a day helped most in a field trial, Fitz et al. 2019), one notification says which sites have news, "WhatsApp (3) · Discord (1)", with Open the Porch. Only the sites of those hours, on the computer you are at (sioul_sync::lease, as the medicines).
  • A site whose area does not fit the hours (areas.md) keeps its notifications, real time and calls included, until its hours come.

Mail that announces a site

A message whose sender belongs to a site's announced_by ("you have a new message in your secure space") gets a button above its headers: "Open ".

Security keys and one-time codes

  • A security key (WebAuthn, FIDO2: a YubiKey for GitHub, Proton Mail, Google…): when a site asks, Sioul's dialog says which of the key's accounts, asks its PIN (or a new one when the key has none or asks to change it, with how many tries are left), says to touch it again, and says in plain words why it failed (not registered, PIN blocked, timeout…), with Retry. A plain touch shows nothing: the key blinks and the page waits (Qt 6.11 says nothing then). Passkeys kept on the key work; passkeys in a phone or in the system do not on Linux and macOS (Qt WebEngine offers none); on Windows, Windows' own dialog takes all of it.
  • A fix for Qt 6.10–6.11: sites that ask PublicKeyCredential.getClientCapabilities() waited forever for an answer that never came (QTBUG-149575); a one-line script, in every page before its own, takes that question away, and the site asks the key directly.
  • Linux: works with Qt WebEngine built with udev (Fedora's is) and systemd's rules for keys (no udev rule to add). The Flatpak needs --device=all to reach /dev/hidraw*.
  • One-time codes: "Fill the login" on a page asking for a code writes the code the vault's secret gives now, only on the site its login belongs to, only when you click.

Bitwarden

Logins are filled from Bitwarden, read by Sioul itself (sioul_sync::bitwarden): nothing to install, on every system. The account (its e-mail, and its server when it is not bitwarden.com: bitwarden.eu, or your own, Vaultwarden too, always over HTTPS) goes in Sites ⚙. The key settings a server gives are held to Bitwarden's own bounds, so a server cannot weaken them. "Fill the login" asks for the master password once per session; it derives the master key as Bitwarden's clients do (PBKDF2-SHA256 or Argon2id, as the account says), logs in with its hash, opens the user key (HKDF-stretched master key; AES-256-CBC checked by HMAC-SHA256 before anything is decrypted), an organisation's key through the RSA key, an item's own key when it has one, and newer accounts' COSE messages (XChaCha20-Poly1305, AES-256-GCM; XAES-256-GCM not yet). The password and the keys are dropped; the logins stay in memory, wiped when the vault closes. Read only: nothing is ever written to the vault. Bitwarden refuses clients too far behind its own version ("Please update your app to continue using Bitwarden": a declared 2024.12.0 was refused in October 2026), so Sioul declares itself Bitwarden's desktop app at the server's own version, read from its /config once a session (2026.9.0 when the server does not say).

  • The security key alone (Bitwarden's "log in with passkey", for a key whose passkey is used for encryption: Settings ▸ Security ▸ Master password in Bitwarden's web vault): "Open with my security key", above the password; what opened the vault last time is proposed first. Sioul asks Bitwarden for a login's options (accounts/webauthn/assertion-options, asked of no account: the key says whose it is), then asks the key at once, as for a second step (below), for the passkey (its PIN in Sioul's dialog) and its secret (WebAuthn PRF, salted with SHA-256 of "passwordless-login", as Bitwarden's apps salt it). The answer waits in the page until Sioul takes it from there; the secret never goes in an address, which would carry it out. Bitwarden checks the key's signature (grant_type=webauthn) and gives back an RSA key sealed by the secret and the user key sealed by that RSA key (WebAuthnPrfOption): the secret, stretched as a master key is (HKDF "enc" and "mac"), opens the RSA key, which opens the user key (RSA-OAEP-SHA1); the rest is as with the password. No second step and no new-device code: Bitwarden counts the key's PIN as a second factor. A passkey of another Bitwarden account than the one in Sites ⚙ is refused (the access token names its e-mail). When the key gives no secret, when its encryption is off, or when Bitwarden does not know the key, the dialog says so and what to do in Bitwarden.
  • A second step, the account's own first, in the order Bitwarden's apps propose them, any other one choice away:
    • a security key (a YubiKey or any FIDO2 key, as Bitwarden registers them now), asked at once when its step comes. A key signs only for the page it is asked from, and Bitwarden takes signatures for its vault's address only (ServerDomain and Origins): so the vault's own light security-key page (webauthn-connector.html, nothing shown unasked) is loaded, held unseen in the dialog (a one-point view, given the focus: a page asks a key only while it has it), and Sioul's script (bitwarden-key.js) asks the key there with Bitwarden's options as Bitwarden's page passes them (the AppID extension of keys registered as U2F included), then writes the answer word for word as that page writes it (clientDataJson, the extension results): the step's code. The PIN, when the key has one, comes in Sioul's own dialog, above; a plain touch shows nothing, the key blinks. "Stop" drops the page; "Use the security key" asks again. No separate window: one would cover the PIN's dialog;
    • a YubiKey's code (YubiKey OTP: it types it), an authenticator app, e-mail (the code is sent when this step is chosen, "Send the code again" beside it), a recovery code.
    • Which steps come is the server's choice: a security key registered as FIDO2 is offered to every account; "YubiKey OTP" and Duo only while the account has Premium (Bitwarden hides them from every app otherwise, TwoFactorProvider.RequiresPremium). When no key is offered, the dialog says so and why.
    • "Remember this device" is kept in the system keyring, so the next sessions skip the step. Duo cannot be taken here. A new device's code, mailed by Bitwarden, is asked the same way. This computer's identifier is kept ($XDG_STATE_HOME/sioul/bitwarden-device), so it is a new device once.
  • Which login: the site's only one is filled at once; several, or none, open a list, the one chosen last for that site first (kept in $XDG_STATE_HOME/sioul/bitwarden-chosen.json, the site's host → Bitwarden's item id), with a search over the whole vault (name, user name, sites; case and accents aside); ⋮ ▸ "Choose a login…" opens it any time. The list holds names and user names only: a password leaves the vault for the one login chosen. A login made for another domain than the page's says which ("for bank.example.net"), so a look-alike site shows.
  • The form: the login is written into the page's password field and the name field before it, as typing would (the values travel as JSON, so nothing in them can change the script); on a page asking for a one-time code, the code the vault's secret gives now (RFC 6238: base32, otpauth:// with its digits, period and SHA-1/256/512, Steam's five characters).
  • Checked against Bitwarden's own key-derivation test vectors (PBKDF2, Argon2id, HKDF), RFC 6238's codes, a changed byte refused before decryption, and a COSE message; against the live cloud with a made-up account (refused for its password, not its version: cargo test -p sioul-sync the_cloud_takes_the_version -- --ignored); a passkey's keys made as Bitwarden's web vault makes them, opened by its secret and refused for another key's; the cloud's passkey options (the_cloud_asks_a_passkey, ignored: it asks Bitwarden); offscreen, Bitwarden's page held unseen in the dialog, focused as a key needs, the key asked at once, for both ways; a refusal and an answer without secret taken back to the dialog; the chooser on a made-up vault (SIOUL_TEST_VAULT, test builds only): a site's two accounts, a search, the last chosen first, another domain said. Not yet with a real key and vault: Qt WebEngine's support of the PRF extension is the first thing a real key shows.