- [ ] Adapt `surfsense_web/docker-entrypoint.js` placeholder replacement for Electron:
- Build Next.js with placeholder values (e.g. `__NEXT_PUBLIC_FASTAPI_BACKEND_URL__`)
- On Electron startup, before spawning the Next.js server, run the same string replacement on `.next/standalone/` files
- Read target values from `electron-store` (self-hosted) or use hosted defaults (cloud)
- [ ] Add "Self-Hosted?" link at the bottom of the login page:
- Clicking reveals Backend URL and Electric URL input fields
- User fills in their URLs, clicks Save → stored in `electron-store`
- Electron re-runs placeholder replacement with new values, restarts the Next.js server
- This link is only visible in the Electron app (detect via `window.electronAPI`)
- Hosted users never see or interact with this
- [ ]`INTERNAL_FASTAPI_BACKEND_URL` (used in `verify-token/route.ts`, defaults to `http://backend:8000`): Set via `process.env` before spawning the Next.js server. For hosted builds, use the production backend URL. For self-hosted, use the same URL the user configured.
### 2.3 — Handle the contact form API route
- [ ]`surfsense_web/app/api/contact/route.ts` uses Drizzle ORM with `DATABASE_URL` to insert directly into PostgreSQL
- [ ] Desktop app does NOT have a direct PostgreSQL connection
- [ ] The contact form is a landing page feature. Desktop app starts at `/login` → never hits this route. Verify this is the case.
- [ ] If reachable: make it return 503 when `DATABASE_URL` is unset
### 2.4 — PostHog analytics in desktop
- [ ] PostHog is initialized only when `NEXT_PUBLIC_POSTHOG_KEY` is set
- [ ] Decision: Build with `NEXT_PUBLIC_POSTHOG_KEY=""` to disable in v1. Re-enable in v2 with `platform: 'desktop'` property.
---
## Phase 3: Authentication
### 3.1 — Email/password login (should work as-is)
- [ ] Verify login page renders in Electron
- [ ] Verify POST to `{BACKEND_URL}/auth/jwt/login` succeeds
- [ ] Verify token stored in localStorage (`surfsense_bearer_token`, `surfsense_refresh_token`)
- [ ] Verify `authenticatedFetch` includes Bearer token
- [ ] Verify token refresh on 401
- [ ] Verify logout clears tokens and redirects to `/login`
### 3.2 — Google OAuth login via deep link
- [ ] Register `surfsense://` protocol via `app.setAsDefaultProtocolClient("surfsense")`
- [ ] Intercept "Login with Google" → open in system browser via `shell.openExternal()`
- [ ] Append `?source=desktop&redirect_uri=surfsense://auth/callback` to authorize URL
- [ ]**Backend change** (`users.py`, ~5 lines): If `redirect_uri` starts with `surfsense://`, redirect there instead of `{NEXT_FRONTEND_URL}/auth/callback`
- [ ] Handle deep link in Electron:
- macOS: `app.on('open-url')`
- Windows/Linux: `app.on('second-instance')`
- Parse URL, extract tokens, inject into renderer, navigate to `/dashboard`
- [ ] Platform notes:
- macOS: requires packaged `.app` (not `electron .` dev mode)
- Windows: works in dev mode
- Linux `.deb`: registers `.desktop` file with `MimeType=x-scheme-handler/surfsense;`
- Linux AppImage: known issues on some DEs
### 3.3 — Secure token storage
- [ ] v1: Use localStorage (matches web behavior)
- [ ] v2: Upgrade to `electron.safeStorage` for encrypted storage
### 3.4 — Handle `?source=desktop` on the web
- [ ]`TokenHandler.tsx` checks for `source=desktop` param
- [ ] If present: redirect to `surfsense://auth/callback?token=...&refresh_token=...`
**Total: ~16 lines of backend changes. Zero connector changes. Zero frontend changes (except optional `electronAPI` detection).**
---
## Decisions (Resolved)
### 1. `next-electron-rsc` vs child process?
**Decision: Child process (spawn `node server.js`).**
Standard pattern, no third-party dependency risk. Used by CodePilot (4,155 stars).
### 2. Runtime config for self-hosted users?
**Decision: Single build. Adapt `docker-entrypoint.js` placeholder pattern for Electron.**
- Hosted users: placeholders replaced with hosted defaults automatically. App goes straight to `/login`. No config UI.
- Self-hosted users: "Self-Hosted?" link at bottom of login page reveals Backend URL and Electric URL fields. Stored in `electron-store`, persists across restarts.
- Single build = 3 installers (not 6), one CI pipeline.
### 3. Which Electron version to target?
**Decision: Latest stable at time of implementation.**
Verify Node.js >= 18.18 (Next.js 16 requirement) and Chromium WASM support for PGlite.
### 4. Secure token storage in v1?
**Decision: localStorage in v1.**
Matches web behavior. Upgrade to `safeStorage` in v2.
### 5. Landing page in desktop app?
**Decision: Skip it. Start at `/login` (or `/dashboard` if authenticated).**
Landing page contact form needs `DATABASE_URL` — irrelevant for desktop users.
### 6. Separate builds for hosted vs self-hosted?
**Decision: Single build.**
Uses `docker-entrypoint.js` placeholder pattern for runtime URL configuration. Hosted defaults applied automatically. Self-hosted users configure via "Self-Hosted?" link on the login page. One build, 3 installers (macOS/Windows/Linux), one CI pipeline.
### 7. Packaging tool?
**Decision: electron-builder (not Electron Forge).**
Electron Forge is official but designed to manage the frontend build (Vite/webpack). We use Next.js which manages its own build. electron-builder is "bring your own build" friendly. Used by CodePilot (4,155 stars) and DarkGuy10 boilerplate (87 stars).
### 8. TypeScript compilation?
**Decision: esbuild (via custom build script).**
Used by CodePilot. Faster than `tsc`, simpler than `tsup`/`tsdown`. Only compiles 2-3 Electron files.