Authentication¶
tldrq has no passwords: users sign in with GitHub or Google through Supabase Auth, and the session lives in cookies
that @supabase/ssr manages. The desktop app signs in through the system browser and hands the session back to
the app.
Supabase clients¶
| File | Function | Used by |
|---|---|---|
src/lib/supabase/server.ts |
createClient() (async): createServerClient with await cookies() |
Server components and route handlers |
src/lib/supabase/client.ts |
createClient(): createBrowserClient |
The browser: starting OAuth, setSession, signing out |
src/lib/supabase/middleware.ts |
updateSession(request): createServerClient over the request's cookies |
The proxy |
The server client's setAll swallows errors: Server Components can't set cookies, and the proxy refreshes the
session anyway.
The proxy¶
src/proxy.ts (the Next.js 16 name for middleware) runs updateSession on every request except _next/static,
_next/image, favicon.ico and image files (svg, png, jpg, jpeg, gif, webp).
updateSession calls supabase.auth.getUser(), which refreshes the session and writes the refreshed cookies to
the response. If there's no user, any path other than /login, /auth/* and /share/* is redirected to /login.
That includes /api/*: a signed-out API call gets the redirect before its handler's own 401 check runs.
src/app/(main)/layout.tsx checks getUser() again before rendering the dashboard.
Web sign-in¶
sequenceDiagram
participant B as Browser
participant T as tldrq
participant S as Supabase Auth
participant O as GitHub or Google
B->>S: signInWithOAuth, redirectTo origin + /auth/callback
S->>O: authorize
O-->>S: approved
S-->>B: redirect to /auth/callback with a code
B->>T: GET /auth/callback
T->>S: exchangeCodeForSession(code)
T->>T: create missing default collections, notify Slack
T-->>B: redirect to / with session cookies
LoginButtons(src/components/auth/login-buttons.tsx) callssignInWithOAuth({ provider, options: { redirectTo: origin + "/auth/callback" } }).- Supabase sends the user to GitHub or Google and back, then to
/auth/callback?code=…. src/app/auth/callback/route.tscallsexchangeCodeForSession(code), which sets the session cookies, creates any missing default collections, sends the Slack notice, and redirects to thenextquery parameter if there is one (nothing in the app sets it), else/.- With no code, or a failed exchange, it redirects to
/login?error=auth.LoginButtonsthen shows theerror_descriptionfrom the URL's hash if Supabase put one there, else "Authentication failed. Please try again."
Default collections and notifications¶
Both callbacks, /auth/callback and /auth/callback-desktop, select the user's collections named Videos, Tweets,
Articles and Repos, insert the ones that are missing, and call notifySlackUserLogin (see
Slack sign-in notifications). The two route handlers duplicate this
code, so change both together.
Desktop sign-in¶
Electron's window doesn't handle the providers' passkey and PKCE flows well, so the desktop app signs in through the system browser and gets the tokens back over a loopback HTTP server.
sequenceDiagram
participant A as Electron app
participant L as Local server 127.0.0.1
participant B as System browser
participant T as tldrq.com
participant S as Supabase and provider
A->>A: LoginButtons sees Electron, calls window.open
A->>B: openExternal /auth/login-desktop with provider and port
B->>T: GET /auth/login-desktop
B->>B: signOut local, then signInWithOAuth
B->>S: OAuth
S-->>B: redirect to /auth/callback-desktop with code and port
B->>T: GET /auth/callback-desktop
T->>S: exchangeCodeForSession(code)
T-->>B: redirect to 127.0.0.1 port /auth/callback with tokens
B->>L: GET /auth/callback with tokens
L-->>B: Sign-in successful page
L->>A: load tldrq.com /auth/desktop with tokens
A->>T: /auth/desktop calls setSession
A->>A: router.replace to /
- In the app,
LoginButtonsseesElectronin the user agent and, instead of starting OAuth, callswindow.open(origin + "/auth/login-desktop?provider=…"). setWindowOpenHandlerinelectron/main.jsappends&port=and the local server's port, and opens the URL in the default browser withshell.openExternal.- In the browser,
src/app/auth/login-desktop/page.tsxcallssignOut({ scope: "local" })to clear any stale session or PKCE state, thensignInWithOAuthwithredirectTo: origin + "/auth/callback-desktop?port=…". - After the provider,
src/app/auth/callback-desktop/route.tsexchanges the code (in the browser's request), creates missing default collections, sends the Slack notice, and redirects tohttp://127.0.0.1:<port>/auth/callback?access_token=…&refresh_token=…. - The app's local server, listening on
127.0.0.1at a random port since the app started, answers with a page reading "Sign-in successful! You can close this tab." and loadshttps://tldrq.com/auth/desktop?access_token=…&refresh_token=…in the main window. src/app/auth/desktop/page.tsxsees the Electron user agent, callssupabase.auth.setSessionwith the two tokens, androuter.replace("/").
The tokens travel in URL query strings: the system browser's redirect to the loopback address leaves them in its
history, and the app's request for /auth/desktop sends them to tldrq.com in the URL.
Step 4 exchanges the code in the system browser's request, so it also stores the session in that browser's cookies, and the app then uses the same tokens. The app and that browser therefore share one session: a local-scope sign-out in either one ends it for both. Step 3's sign-out also ends whatever session the browser had before.
Fallbacks¶
will-navigateinelectron/main.jscancels any navigation to asupabase.co/authURL inside the app. When the URL has aproviderparameter, it reopens it as/auth/login-desktop?provider=…&port=…in the system browser./auth/callback-desktopwithout aportjust redirects to/./auth/desktopopened outside Electron tries atldrq://auth/callback?…link and, after 1.5 seconds, shows "Sign-in successful! You can close this tab and return to the tldrq app." The app doesn't register atldrq://handler (electron/main.jshas none), so the loopback server is the path that actually completes sign-in.
Signing out¶
SignOutButton (src/components/layout/sign-out-button.tsx) calls signOut({ scope: "local" }) and
router.push("/login"). The local scope ends only the current session; Supabase's default, global scope would end
every session the user has, on every device. /auth/login-desktop uses the local scope for the same reason.
Supabase redirect URLs¶
Sign-in returns to whatever origin it started from, so Supabase's redirect URL list needs, for every origin users
sign in from, <origin>/auth/callback, plus <origin>/auth/callback-desktop where the desktop app is used. For
example:
https://tldrq.com/auth/callback
https://tldrq.com/auth/callback-desktop
http://localhost:3000/auth/callback
Deployment covers the rest of the Supabase setup.