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:
- The browser blocks the link (even with a click, some browsers are more aggressive)
- macOS Gatekeeper blocks unsigned/untrusted apps from receiving links
- The app isn't installed in
/Applications(common during development) - The user dismisses the macOS "Do you want to open Vitto?" dialog
- 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?
- If the deep link works (Path A): the user is signed in instantly — no delay
- If the deep link fails (Path B takes over): the user waits at most 2 seconds for the server to deliver the credentials
- No user action required either way — it's automatic and invisible
- Same approach used by VS Code, Slack, 1Password — industry-proven pattern
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:
- Before: Only tried to open the
vitto://link (fragile) - 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":
- Before: Opened the browser and waited for the deep link (single path)
- 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
- Users affected: All Vitto Desktop users who sign in via the "GoGrab" tab
- Improvement: Sign-in now works even when the deep link fails (browser blocking, unsigned app, dev mode)
- Risk: Low — all changes are additive. Deep link path is preserved unchanged. Server polling is a parallel fallback
- Email/Phone sign-in: Completely unaffected — those paths don't use deep links
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 |