Architecture
The three-process model, Conveyor IPC, feature flags, and startup flow.
The template is organized around Electron's security boundary: privileged Node code stays in the main process, a small preload bridge exposes approved APIs, and the renderer behaves like a sandboxed React app.
Three processes
| Process | Owns | Must not own |
|---|---|---|
| Main | App lifecycle, windows, menus, tray, protocols, settings file, update service, native notifications, secure storage, file dialogs, logging, diagnostics. | React UI and product screens. |
| Preload | The contextBridge surface that exposes window.conveyor to the renderer. | Business logic, broad Node APIs, ad hoc IPC. |
| Renderer | React routes, UI state, forms, Supabase client calls, TanStack Query, command palette, theme and i18n UI. | Direct filesystem, shell, ipcRenderer, or unrestricted Node access. |
lib/main/app.ts creates the BrowserWindow with sandbox: true,
contextIsolation: true, nodeIntegration: false, and webSecurity: true.
That is the default posture to keep when adding product features.
Conveyor in one screen
Renderer code calls typed methods:
const { getSettings, setSettings } = useConveyor('app')
const settings = await getSettings()
await setSettings({ theme: 'dark' })The call path is:
React component -> window.conveyor.app.setSettings()
-> preload contextBridge
-> ipcMain handler in lib/conveyor/handlers/app-handler.ts
-> Zod-validated args and Zod-validated return value
-> typed result back to the rendererlib/main/shared.ts wraps ipcMain.handle() and validates both request args
and handler return data with schemas from lib/conveyor/schemas/. Main-to-
renderer events, such as app intents and update status, are also validated
before dispatch.
There is no codegen step. TypeScript infers channel types from the schema map and the exported Conveyor API object.
Do not import or expose ipcRenderer in app code. Add a Conveyor schema, API
method, and handler instead so the process boundary remains typed and runtime
validated.
Feature flags
Renderer feature flags are read centrally in shared/config.ts. The main
process has its own showcase flag in lib/main/feature-flags.ts so native menu
entries can disappear with the renderer demo layer.
| Flag | Default | Process | Effect |
|---|---|---|---|
VITE_AUTH_ENABLED | true | Renderer | Shows Supabase auth/account surfaces. The current renderer still validates the Supabase URL/key before mount. |
VITE_AUTO_UPDATE_ENABLED | true | Renderer | Shows update settings and update route surfaces. |
VITE_BILLING_ENABLED | false | Renderer | Enables Billing page actions, checkout, subscription reads, and feature gates. |
VITE_BILLING_LICENSE_MODE | false | Renderer | Enables optional Creem license-key activation UI and service calls. |
VITE_SHOWCASE_ENABLED | true | Renderer | Enables Showcase routes, sidebar entries, command palette actions, and Home demo sections. |
MAIN_VITE_SHOWCASE_ENABLED | falls back to VITE_SHOWCASE_ENABLED, then true | Main | Enables native Template menu items. Keep it matched with the renderer showcase flag. |
Billing is the cleanest dark subsystem today: when billing is off, the
subscription store switches to disabled and does not read Supabase. Showcase
is also isolated behind flags and [template-demo] markers.
State layers
| Layer | Files | Notes |
|---|---|---|
| Persistent desktop settings | lib/main/settings.ts | Versioned JSON stored under Electron userData, migrated forward, read/written over Conveyor. |
| Auth session and profile | app/stores/auth-store.ts, app/lib/supabase/* | Supabase session persisted through a secure-session adapter backed by main-process safeStorage. |
| Entitlement state | app/stores/subscription-store.ts | Reads the subscriptions table only when billing is enabled; never treats an unknown network state as paid. |
| Server data | app/services/*, app/lib/query/client.ts | Service functions call Supabase or Edge Functions; TanStack Query owns cache behavior in UI code. |
| UI preferences | app/utils/theme-settings.ts, settings store | Theme, accent, locale, zoom, proxy, and update preferences flow through the settings API. |
Use services as the swap point for product data. Components should not embed SQL-shaped Supabase calls when a service module can own that contract.
Startup flow
lib/main/main.tsenables Electron sandboxing, initializes logging and optional error reporting, and creates the settings store.- The app registers the
soar-electron://protocol handler. In development it passes Electron's binary plus the project path; packaged builds register the executable directly. initializeApplication()wires app services: i18n, secure storage, window state, app intents, updater, tray, notifications, shortcuts, menu, resource protocol, permissions, and Conveyor handlers.createPrimaryWindow()builds the sandboxedBrowserWindow, restores bounds, applies zoom, blocks unsafe navigation, and loads the dev server or packaged renderer.app/renderer.tsxvalidates Supabase env, bootstraps i18n through Conveyor, mounts React, and installs renderer error logging.app/app.tsxloads theme/accent preferences, initializes auth and subscription stores, tracks route analytics when configured, and handles app intents.OnboardingGatereads persisted settings and shows first-run onboarding until completion.
Deep links and second-instance launches are normalized into typed app intents.
The route allow-list lives in lib/conveyor/schemas/event-schema.ts, so unknown
deep-link routes are rejected before the renderer sees them.
What belongs where
| Task | Put it here |
|---|---|
| Add a React route, page, form, or product workflow | app/routes, app/components, app/services, app/lib/query |
| Read or write local files, settings, OS keychain, tray, menus, shortcuts, or notifications | lib/main behind a Conveyor handler |
| Expose a new main-process capability to React | lib/conveyor/schemas, lib/conveyor/api, lib/conveyor/handlers, then register in lib/main/main.ts |
| Share a brand constant, feature flag, or price-plan mapping | shared/ |
| Add server-side billing behavior | supabase/functions and Supabase SQL/RLS |
