Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Huskarl AI colleagues on your own Mac Starf An activity monitor for Apple silicon that never invents a number etrans An SSH and SFTP client for macOS Elyra VM Virtual machines for macOS, Linux and Windows on your Mac Litr A small, native web browser for macOS Notr A notebook for macOS e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Refr Local-first PDF workspace for macOS Elyra Workspace A desktop workspace for coding agents Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Start here
Concepts
Release notes
What's new
Elyra
The desktop app

The desktop app

Signing the desktop app in through the browser, what it is given, and the protocol for anybody writing a client against it.

The server half is in place; the app itself is still being built. Everything on this page describes behaviour Félagi has today, and the app is written against it.


Signing in

  1. Choose Sign in in the app. It shows an eight-letter code, BCDF-GHJK style, and opens your browser at /app/desktop/link with the code filled in.
  2. Sign in to Félagi in that browser if you are not already — however you normally do: password, single sign-on, two-factor.
  3. Check that the device named on the page is the one in front of you, and that it asked a moment ago.
  4. Tick the workspaces the app should reach. The one you are working in is ticked already.
  5. Approve. Go back to the app; it notices within a couple of seconds.

Only approve a sign-in you started yourself, on your own computer. The page says so in as many words, because that is the one way this flow is attacked: nobody can guess a code, but somebody can send you one with a plausible reason and a link. If a code arrived in a message, refuse it — Deny gives the app nothing — and tell whoever runs your workspace.

A code lasts ten minutes and works once. After that, start again from the app.


Why the app never asks for your password

Because it would be the worst place to type it.

  • Plenty of people do not have one. An account that signs in with single sign-on has nothing to give the app, and teaching a desktop app to drive somebody's identity provider is a project of its own.
  • Two-factor would have to be rebuilt inside the app, grace periods and recovery codes included, and kept in step with the browser for ever.
  • A password on a disk unlocks everything, including changing the password — the argument Working from your editor makes for tokens.

So the decision is made in the browser, where all of that already happens. The page lives inside the signed-in application, behind the same middleware as the board, so whatever stood between you and the board stood between you and Approve — and the app never has to know which of it applied.

This is the OAuth 2.0 device authorization grant (RFC 8628), the flow a television uses to sign in to a streaming service. It suits a desktop app for the same reason: the device cannot be trusted with the password, and the person already has a browser that can.


What the app is given

One ordinary API token per workspace you ticked. Not a new kind of credential: a token that names exactly one workspace is the principle the API is built on, and an app on your desk is not a reason to invent one that reaches all of them.

Each token:

  • is called after the device — Astrid’s MacBook · Félagi Desktop — and appears under Settings → API tokens, where you can revoke it like any other;
  • has write access, because the app is for doing the work, not reading about it;
  • has no expiry. It stops working when you revoke it, when the app signs out, or when you leave the workspace — membership is checked on every request;
  • acts as you, and can never do more than you can in the browser.

Signing out revokes them. The app calls DELETE /api/v1/me/token with each, so nothing is left behind in the keychain of a machine you have stopped using.

Who can approve, and for which workspaces

Only workspaces where your role works on issues are offered. Management is not: the API refuses that role on every request, so a token for it would be a credential that cannot work. Somebody who is Management everywhere is turned away by the page with a 403 rather than offered an empty list.


The protocol

For anybody writing a client. Two endpoints, both unauthenticated — the app has nothing to authenticate with yet — and both outside /api/v1.

1. Ask — POST /api/desktop/device

curl -X POST "$FELAGI_URL/api/desktop/device" \
  -H 'Content-Type: application/json' \
  -d '{"device_name": "Astrid’s MacBook"}'

device_name is required, at most 100 characters, and is what the person sees on the approval page. Make it something they will recognise. 201:

{
  "device_code": "<48 characters>",
  "user_code": "BCDF-GHJK",
  "verification_uri": "https://felagi.example.com/app/desktop/link",
  "verification_uri_complete": "https://felagi.example.com/app/desktop/link?code=BCDF-GHJK",
  "expires_in": 600,
  "interval": 2
}
  • device_code is the secret the app polls with. Keep it in memory; Félagi stores only a hash.
  • user_code is for the person. Show it large. It is eight letters from twenty consonants — no vowels, so it never spells a word, and no characters that look alike — and the page accepts it in any case, with or without the dash.
  • verification_uri_complete is the one to open in the browser. The plain verification_uri is for showing, in case the browser does not open.
  • Both URLs are built from the configured APP_URL, not from the request, so they are the address people actually use even behind a proxy.

2. Wait — POST /api/desktop/token

curl -X POST "$FELAGI_URL/api/desktop/token" \
  -H 'Content-Type: application/json' \
  -d '{"device_code": "<48 characters>"}'

Poll every interval seconds until something other than 202 comes back, or until expires_in has passed.

Status Body Meaning
202 {"status": "pending"} Nobody has answered yet. Ask again in interval seconds
200 the tokens, below Approved. Delivered once — store them now
403 {"message": "…"} Refused in the browser — or every workspace chosen was lost before the app collected
404 {"message": "…"} Unknown, expired, or already collected. Start again from step 1
422 validation errors No device_code in the body
429 Polling too fast. Retry-After says how long

The 200:

{
  "status": "approved",
  "user": { "id": 1, "name": "Astrid", "email": "astrid@example.com", "locale": "nb" },
  "workspaces": [
    { "id": 1, "name": "Acme", "slug": "acme", "issue_prefix": "ACM", "role": "owner", "token": "fat_…" }
  ]
}

user.locale is nb, en or null; null means follow the operating system. The app can change it later with PATCH /api/v1/me — see Me.

The tokens are handed out exactly once. Collecting them marks the request used in the same database statement that checks it was not, so two polls racing each other — a retry on a flaky network does this — get the tokens and a 404 between them, never two sets. A client that loses the response has to sign in again.

Membership is asked again at that moment. Minutes pass between approving and collecting; a workspace you left, or where you became Management, in those minutes is silently dropped from the list. If that empties it, the answer is 403.

404 is one answer for three things on purpose. Telling "never existed" from "expired" from "already used" would tell somebody trying device codes which of their guesses was once real.

3. Listen — GET /api/desktop/broadcasting

With a token in hand, the app asks where the websocket server is. The browser has this baked in at build time; the app is one binary for every Félagi and learns it here.

{ "key": "app-key", "host": "ws.example.com", "port": 443, "scheme": "wss" }

host, port and scheme are where a client connects (VITE_REVERB_*, falling back to REVERB_*), which behind a proxy is not where Laravel publishes. 404 means this installation does not broadcast: the app works without live updates rather than retrying. Private channels are then authorised at POST /api/broadcasting/auth with the same token — see Chat.

Limits

Limit Keyed on
POST /api/desktop/device 10 a minute Address
POST /api/desktop/token 60 a minute Address — twice what one app polling every two seconds needs
Codes typed on the approval page 10 a minute The signed-in person

The page throttle is what makes eight letters enough: twenty to the eighth is twenty-five billion codes, against ten guesses a minute for each account that can sign in at all.


Housekeeping

Every request expires ten minutes after it was made, answered or not. The scheduler prunes them a day after that — model:prune for DesktopAuthorization, nightly at 02:30 — so a question about this morning's sign-in still has something to look at. See Running it.