S290 Progress Report

Documentation for Sessions

S290: Making Vitto Desktop Sign-In Reliable — Progress Report

Date: March 30, 2026 Session: 290 Depends on: S288 (deep link fixes) — no blocking dependency For: Team overview — written in plain language


Background: What S288 Fixed

In Session 288, we fixed the immediate problem: when users signed in via the browser, the credentials couldn't get back to the Vitto desktop app because modern browsers silently block certain types of links without a user click. S288 added a clickable "Open Vitto" button so the browser wouldn't block the handoff.

But the underlying fragility remained. The entire flow still depends on a "deep link" (vitto://auth?token=...) — a special link that the operating system routes from the browser to the desktop app. Deep links can fail for many reasons that are outside our control:

  1. The browser blocks the link (even with a click, some browsers are more aggressive)
  2. macOS Gatekeeper blocks unsigned/untrusted apps from receiving links
  3. The app isn't installed in /Applications (common during development)
  4. The user dismisses the macOS "Do you want to open Vitto?" dialog
  5. The link type isn't registered properly in the app's configuration

Session 290 solves this permanently by adding a second, completely independent channel.


What We Built (Session 290)

The Solution: A Server-Side Backup Channel

We added a backup method for delivering sign-in credentials from the browser to the desktop app. Instead of relying solely on the special vitto:// link, the browser and desktop app now also communicate through our server:

HOW IT WORKS:

1. User clicks "Sign in with GoGrab" in Vitto desktop
2. Vitto creates a unique session ID (a random code)
3. Browser opens to the sign-in page (with the session ID in the URL)
4. User signs in normally

   Then TWO things happen at the same time:

   PATH A (existing, fast):
   Browser sends credentials via vitto:// link
   → macOS routes to Vitto app
   → Instant delivery (when it works)

   PATH B (new, reliable):
   Browser uploads credentials to our server (tagged with the session ID)
   Meanwhile, Vitto desktop checks the server every 2 seconds
   → Server delivers the credentials
   → Works every time (standard web request)

5. WHICHEVER PATH DELIVERS FIRST WINS
   The other path is automatically cancelled.

Why This Approach?

The deep link isn't removed — it's kept as the fast path. The server polling is the reliable backup. Together, they cover every failure scenario.


Timeline of Work

Step Status What It Achieves
1. Read spec and explore codebase Done Understood all 6 files to create/modify, existing auth flow, and integration points
2. Design questions and alignment Done Confirmed 5 design decisions with the team before writing code
3. Build server endpoint (backend) Done New endpoint on the backend server that receives and stores credentials temporarily (3-minute window)
4. Build exchange service (frontend) Done New service that handles both uploading credentials (browser side) and polling for them (desktop side)
5. Update browser sign-in page Done After user signs in, the page now also uploads credentials to the server (alongside the existing deep link)
6. Update desktop welcome screen Done When user clicks "Sign in with GoGrab", Vitto now starts both the deep link listener AND server polling simultaneously
7. Code review and bug fixes Done 3 bugs caught and fixed during review (see below)
8. Documentation and intelligence capture Done This report, technical docs, 6 decisions logged, 4 conventions added
9. Build, test, and deploy Pending Need to build the Tauri app and test all sign-in paths

What Changed (For Non-Developers)

New: Server Exchange Endpoint

A new endpoint on the backend server that acts as a temporary "mailbox." The browser drops off the credentials, and the desktop app picks them up. The mailbox entry self-destructs after 3 minutes and can only be read once — so there's no security risk from credentials sitting on the server.

Updated: Browser Sign-In Page

After the user signs in, the browser now does two things instead of one:

  1. Before: Only tried to open the vitto:// link (fragile)
  2. Now: Also uploads credentials to the server mailbox AND tries the vitto:// link

If the session ID is missing from the URL (e.g., an older version of the desktop app), the page gracefully falls back to the old deep-link-only behavior. No breaking change.

Updated: Desktop Welcome Screen

When the user clicks "Sign in with GoGrab":

  1. Before: Opened the browser and waited for the deep link (single path)
  2. Now: Opens the browser and starts BOTH a deep link listener AND server polling (two parallel paths racing)

Whichever path delivers first wins. The 120-second timeout covers both paths — if neither delivers, the user sees a clear error message suggesting email or phone sign-in.


Bugs Found and Fixed During Review

Three issues were caught during code review before delivery:

1. Native Listener Could Leak Memory

Problem: When the sign-in race ended, the deep link listener inside the desktop app might not get properly cleaned up due to a timing issue with how the listener is initialized (it's an asynchronous operation). Fix: Changed the cleanup code to properly wait for initialization before cleaning up, guaranteeing no listener leaks regardless of timing.

2. Polling Could Silently Give Up

Problem: If the server never received the credentials (e.g., network error on the browser's upload), the polling would quietly stop after 60 attempts without showing any error message. The user would be stuck on "Waiting for browser..." forever. Fix: Added explicit error handling so when polling exhausts all attempts, it surfaces a clear timeout error that the user can see and act on.

3. Retry Could Be Permanently Blocked

Problem: A safety flag that prevents duplicate uploads was being set before the upload started. If the upload failed due to a network error, the flag was already set, permanently blocking any retry. Fix: Moved the flag to only be set after a successful upload, allowing retry on failure.


Security Considerations

Aspect How It's Handled
Session ID guessing UUID v4 = 2^122 possible values. At 60 requests/minute rate limit, brute force is computationally infeasible
Credential exposure on server 3-minute window, one-time read (deleted after first retrieval), in-memory only (not stored to disk)
Rate limiting Upload: 10/minute per IP. Polling: 60/minute per IP. Prevents abuse
CORS (cross-origin) tauri://localhost already allowed by our CORS configuration — no changes needed
Backward compatibility If session ID is missing, falls back to deep-link-only flow. No breaking change for existing builds

Impact


What's Next

Step Description When
Build and test Build Tauri app (tauri build --debug), install, test all 3 sign-in methods Next session
Test failure modes Verify polling works when deep link is broken (unregister URL scheme) Next session
Test timeout Open browser, don't sign in, wait 120s — verify clean error Next session
Deploy backend Deploy the new exchange endpoint to Railway After testing
Redis migration If backend scales to multiple instances, migrate in-memory store to Redis When needed
Mobile integration Consider adding polling for mobile (Capacitor) auth if deep link issues emerge Future

Files Changed Summary (For Reference)

What File Change
Server mailbox endpoint APIs/backend-api/src/app/auth/desktop-exchange.controller.ts New file — POST (deposit) + GET (retrieve)
Server registration APIs/backend-api/src/app/backend-auth/backend-auth.module.ts Added new controller to the auth module
Exchange service libs/libs-ui/auth-ui/src/lib/services/desktop-auth-exchange.service.ts New file — upload + polling logic
Service export libs/libs-ui/auth-ui/src/index.ts Made new service available to other code
Browser sign-in page apps/gograb-ai/src/app/pages/desktop-auth/desktop-auth.component.ts Added credential upload after login
Desktop welcome screen apps/gograb-ai/src/app/pages/desktop-welcome/desktop-welcome.component.ts Added polling + race logic