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

ProcessOwnsMust not own
MainApp lifecycle, windows, menus, tray, protocols, settings file, update service, native notifications, secure storage, file dialogs, logging, diagnostics.React UI and product screens.
PreloadThe contextBridge surface that exposes window.conveyor to the renderer.Business logic, broad Node APIs, ad hoc IPC.
RendererReact 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 renderer

lib/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.

FlagDefaultProcessEffect
VITE_AUTH_ENABLEDtrueRendererShows Supabase auth/account surfaces. The current renderer still validates the Supabase URL/key before mount.
VITE_AUTO_UPDATE_ENABLEDtrueRendererShows update settings and update route surfaces.
VITE_BILLING_ENABLEDfalseRendererEnables Billing page actions, checkout, subscription reads, and feature gates.
VITE_BILLING_LICENSE_MODEfalseRendererEnables optional Creem license-key activation UI and service calls.
VITE_SHOWCASE_ENABLEDtrueRendererEnables Showcase routes, sidebar entries, command palette actions, and Home demo sections.
MAIN_VITE_SHOWCASE_ENABLEDfalls back to VITE_SHOWCASE_ENABLED, then trueMainEnables 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

LayerFilesNotes
Persistent desktop settingslib/main/settings.tsVersioned JSON stored under Electron userData, migrated forward, read/written over Conveyor.
Auth session and profileapp/stores/auth-store.ts, app/lib/supabase/*Supabase session persisted through a secure-session adapter backed by main-process safeStorage.
Entitlement stateapp/stores/subscription-store.tsReads the subscriptions table only when billing is enabled; never treats an unknown network state as paid.
Server dataapp/services/*, app/lib/query/client.tsService functions call Supabase or Edge Functions; TanStack Query owns cache behavior in UI code.
UI preferencesapp/utils/theme-settings.ts, settings storeTheme, 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

  1. lib/main/main.ts enables Electron sandboxing, initializes logging and optional error reporting, and creates the settings store.
  2. 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.
  3. initializeApplication() wires app services: i18n, secure storage, window state, app intents, updater, tray, notifications, shortcuts, menu, resource protocol, permissions, and Conveyor handlers.
  4. createPrimaryWindow() builds the sandboxed BrowserWindow, restores bounds, applies zoom, blocks unsafe navigation, and loads the dev server or packaged renderer.
  5. app/renderer.tsx validates Supabase env, bootstraps i18n through Conveyor, mounts React, and installs renderer error logging.
  6. app/app.tsx loads theme/accent preferences, initializes auth and subscription stores, tracks route analytics when configured, and handles app intents.
  7. OnboardingGate reads 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

TaskPut it here
Add a React route, page, form, or product workflowapp/routes, app/components, app/services, app/lib/query
Read or write local files, settings, OS keychain, tray, menus, shortcuts, or notificationslib/main behind a Conveyor handler
Expose a new main-process capability to Reactlib/conveyor/schemas, lib/conveyor/api, lib/conveyor/handlers, then register in lib/main/main.ts
Share a brand constant, feature flag, or price-plan mappingshared/
Add server-side billing behaviorsupabase/functions and Supabase SQL/RLS

On this page