Skip to content

Clients

Four apps people actually touch: three Next 15 fronts and one Tauri desktop companion. Every one is an OIDC client of the Identity service — none holds a password, none signs a token. They differ mainly in who uses them and which scope they request.

Client Repo User Port Default scope
Backoffice figestió/backoffice accountant 3000 agency
Customer portal figestió/front customer 3002 workspace
Xifrasoft front xifrasoft/front customer 3001 workspace
Figestió Sync figestió/companion-app accountant (desktop) 1420¹ agency

¹ Vite dev-server port for the webview; the shipped app is a native binary.

How the Next fronts authenticate

The three web clients share one shape (the platform's "centralised token injection" pattern). Login is a server-side OIDC dance; tokens live in httpOnly cookies; a single middleware.ts keeps the session fresh and bails to /logout on a hard 401.

%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, system-ui, sans-serif','fontSize':'13.5px','actorBkg':'#dCe9e8','actorBorder':'#4f8f8d','actorTextColor':'#15302f','actorLineColor':'#9fb0af','signalColor':'#5a6b6a','signalTextColor':'#33403f','noteBkgColor':'#eaf0dc','noteBorderColor':'#7c9a47','noteTextColor':'#27300f'}}}%%
sequenceDiagram
  autonumber
  participant B as Browser
  participant R as /api/auth/* (route handlers)
  participant IdP
  participant API as Product API
  B->>R: GET /api/auth/login
  R-->>B: 302 to IdP /oidc/authorize<br/>(PKCE verifier + state in httpOnly cookies)
  B->>IdP: authorize → hosted login → code
  IdP-->>B: 302 /api/auth/callback?code&state
  B->>R: callback
  R->>IdP: POST /oidc/token (code + verifier)
  IdP-->>R: access + refresh
  R-->>B: set access/refresh httpOnly cookies → dashboard
  B->>API: data (Bearer access, injected server-side)

Cookie naming matters on localhost. Cookies aren't port-scoped, so the three fronts on localhost use distinct names to avoid clobbering each other:

Client Access cookie Refresh cookie
Backoffice fi-access-token fi-refresh-token
Portal fi-portal-access-token fi-portal-refresh-token
Xifrasoft front xs-access-token xs-refresh-token

The middleware contract

Every front's middleware.ts implements the same rules (the platform's canonical auth flow):

  • A valid refresh token counts as "logged in"; the middleware refreshes the access token proactively before it expires (calling /auth/refresh with the refresh token as Bearer).
  • A 401 while holding a session cookie redirects to /logout, which clears the cookies and sends the user to /login.
  • rethrowControlFlow() runs first in catch-all blocks so Next's redirect signal isn't swallowed by error handling.
  • The per-render liveness check is the chokepoint: the backoffice sidenav and the portal layout call /users/me; xifrasoft's layout calls getWorkspace(). A 401 there triggers the logout redirect.

Context selection

After login a front may need to pick which workspace or agency to scope to:

  • Backoffice lists the accountant's agencies (GET /agencies); one → auto-select, many → a /select-agency picker. It then upgrades to an agency-scoped token via /auth/refresh?agency_id=.
  • Portal and Xifrasoft front resolve a workspace: a sole workspace is auto-selected, otherwise a /select-workspace picker calls /auth/refresh?workspace_id=.

Figestió backoffice

The accountant's control room (:3000). Manages customers, the folders assigned to them, notification history, and the Syncthing devices. Built on the platform's four-layer feature structure (data / domain / presentation), with presenters as top-level 'use server' files and class instances serialised to plain objects before crossing Server→Client. Default UI language is Catalan via next-intl (cookie locale, no URL prefix). OIDC migration: done.

Key sections: /dashboard/customers, /dashboard/devices (self-serve Syncthing setup), /dashboard/notifications, /dashboard/admins, /dashboard/profile.

Figestió customer portal

The customer's read-only window (:3002) onto the documents their accountant manages. Slim layout, workspace-scoped, mostly server-side reads. It is the seed of the future unified portal that will also surface the customer's xifrasoft invoices inline (see Integration). Today it is intentionally minimal. OIDC migration: done; product scope still narrow.

Xifrasoft front

The invoicing app (:3001): issued/received invoices, companies, catalog (drag-ordered categories), templates and per-company styling, workspace + member management, and Stripe billing. Workspace-scoped. OIDC migration: done for login / callback / refresh / logout; some onboarding screens (register / verify-email / workspace-setup) still call the legacy product-API auth pending fully IdP-hosted onboarding.

Figestió Sync (the companion app)

A Tauri 2 (Rust core) + React desktop app that hides Syncthing from non-technical accountants. They install one binary, log in through their system browser, pick a folder, and the app syncs silently in the background, living in the system tray.

Trust boundaries

%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, system-ui, sans-serif','fontSize':'14px','lineColor':'#7d8f8e','primaryColor':'#e7efee','primaryTextColor':'#211915','primaryBorderColor':'#5f9b9a'}}}%%
flowchart LR
  web["React webview<br/><small>UI only</small>"]
  rust["Rust core<br/><small>Tauri commands</small>"]
  st["Syncthing sidecar<br/><small>127.0.0.1</small>"]
  kc["OS keychain<br/><small>tokens</small>"]
  idp["IdP"]
  api["figestió API"]

  web -->|"invoke()"| rust
  rust --> st
  rust --> kc
  web -->|"auth-code + PKCE<br/>(system browser + loopback)"| idp
  web -->|"device claim (agency token)"| api

  class web,rust,st core
  class kc,idp,api ext
  classDef core fill:#d9e8e7,stroke:#4f8f8d,color:#15302f;
  classDef ext fill:#f3ece6,stroke:#b39a82,color:#2b211b;

The webview never touches Syncthing or the keychain directly — everything goes through Rust invoke() commands, so the Syncthing API key is never exposed to web code.

What the Rust side does (src-tauri/)

  • Syncthing manager (syncthing.rs) spawns the bundled sidecar with STNOUPGRADE=1, persists its API key + port (mode 0600), and exposes folder/device REST helpers.
  • Tokens (tokens.rs) wrap the OS keychain (keyring, service com.figestio.sync) with an in-process cache so unsigned dev builds don't prompt on every access.
  • OAuth listener (oauth.rs) binds a loopback port (51789, with fallbacks), awaits the browser redirect, validates state, and returns the code.

The desktop OIDC flow

Because there's no web origin to host cookies, the companion is a public OIDC client doing auth-code + PKCE through the system browser with a localhost redirect:

  1. The app mints a PKCE verifier + state and reserves a loopback port via Rust (oauth_bind).
  2. It opens the system browser to the IdP /oidc/authorize.
  3. After login, the IdP redirects to http://127.0.0.1:51789/callback; the Rust listener captures the code (oauth_await_code).
  4. The app exchanges the code at /oidc/token (public client: verifier, no secret), then stores the tokens in the OS keychain.
  5. It resolves the accountant's agency (GET /agencies) and upgrades to an agency-scoped token via /auth/refresh?agency_id=.

PKCE supersedes the old User-Agent binding

The earlier FigestioSync/{version} User-Agent session binding is gone; PKCE + the loopback redirect are what tie the code to this app. The IdP no longer binds sessions to IP or User-Agent. OIDC migration: in progress (the highest-effort client).

Setup & sync

The setup wizard ensures Syncthing is running, fetches the server identity (GET /devices/server-identity), adds the server as a peer, claims the device against the figestió API (POST /devices/claim, idempotent), and creates the synced folder. The status screen polls folder + connection state every few seconds and offers pause/resume; closing the window hides to the tray (sync continues) — only the tray's Quit stops Syncthing.

Two build-time facts worth knowing: VITE_FIGESTIO_API_URL and VITE_OIDC_ISSUER_URL are baked into the bundle (a built app talks to the URLs it was compiled with), and the Syncthing sidecar binary is bundled per target triple. See the companion repo's CLAUDE.md for the full build/CI story.