Desktop application for EGEL-style exam simulation with local + remote license validation.
This repository is organized into two runtime layers:
main/: Electron main process (Node.js, IPC, SQLite, license orchestration)renderer/: React app (authentication UI, simulation workflow, exam screens, history)
- Project Goals
- High-Level Architecture
- Technical Stack
- Runtime Lifecycle
- License System
- Simulation Module
- Folder Structure
- Data Model
- IPC Contract
- Environment Variables
- Setup and Development
- Build and Packaging
- Troubleshooting
- Security Notes
- Known Limitations
- Suggested Next Improvements
The application is designed to:
- Restrict access to licensed users.
- Bind a product key to a device (
machineId) through remote validation. - Persist a local activation record for offline startup checks.
- Provide a configurable exam simulator (area, timer, practice mode).
- Persist user attempts and scores in local frontend state.
Main responsibilities:
- Bootstrap Electron app and create the BrowserWindow.
- Configure secure renderer sandboxing (
contextIsolation,nodeIntegration: false). - Initialize and sync local SQLite schema through Sequelize.
- Register IPC handlers for license operations.
- Execute sensitive operations (remote validation, signature generation/checks).
Primary files:
main/main.jsmain/db/index.jsmain/ipc/index.jsmain/ipc/handlers/licenseActivation.handler.js
The preload script exposes a minimal, controlled API to the renderer via contextBridge.
Why it matters:
- Renderer code does not directly access Node.js APIs.
- IPC surface is constrained to explicit methods.
- Security boundary is clear and auditable.
Main responsibilities:
- Route control and license-based navigation.
- Product key capture and activation UX.
- Exam setup and test flow.
- Attempt history rendering.
- Local UI state handling through Zustand stores.
Entry points:
renderer/src/main.tsx: HashRouter bootstrap.renderer/src/App.tsx: initial license refresh and route mounting.renderer/src/routes/routes.tsx: route table.
- Electron
^27.0.0 - Node.js (CommonJS in main process)
- React
^19.1.0 - TypeScript (renderer)
- Vite
^7.0.5
- SQLite (
sqlite3) - Sequelize (
^6.37.7) - Supabase client (
@supabase/supabase-js)
- Zustand (
^5.0.6) - Zustand persist middleware
- React Router DOM (
^7.6.3)
- Tailwind CSS v4
- Lucide React icons
- React Markdown (question/option rendering)
- concurrently (parallel dev scripts)
- electron-builder (desktop packaging)
- Run
npm run devfrom repository root. - Vite dev server starts in
renderer/. - Electron main process starts and points to
http://localhost:5173. - BrowserWindow opens and IPC handlers are registered.
- Build renderer static assets.
- Package Electron app with
electron-builder. - In packaged mode, Electron loads
renderer/dist/index.html.
The license subsystem is the core access-control layer.
- User enters a UUID product key on
AuthPage. - Renderer calls
window.api.licenseActivation.verifyAndActivateKey(productKey). - Main process obtains
machineIdusingnode-machine-id. - Main process fetches remote license from Supabase (
licensestable). - Validation checks:
- license exists
- license status is
active - license is not bound to a different machine
- If valid, app updates remote record with
machineId+ activation timestamp. - App creates local activation record in SQLite.
- Local record includes an HMAC signature of
{ productKey, machineId }.
- On startup, renderer executes
useLicenseStore.refresh(). - Renderer requests local validation via IPC.
- Main process loads first local activation from SQLite.
- Main process recomputes HMAC using
SIGNATURE_SECRET. - If signature mismatch is detected:
- activation record is deleted
- access is denied
- If valid, user is considered authenticated and redirected to
/home.
- Algorithm: HMAC-SHA256
- Key:
SIGNATURE_SECRET - Payload normalization: keys are recursively sorted before hashing
This design prevents trivial local record tampering.
The simulation flow is entirely in renderer state.
User chooses:
area:disciplinar,transversal, orambastimerEnabled: booleanpracticeMode: booleanduration: currently hardcoded in setup logic
Configuration is stored in useSetupStore.
- Questions are loaded via
useQuestions(). - Question card is rendered by
Questioncomponent. - Markdown is supported in prompts/options.
- Timer counts down if enabled.
- In practice mode, immediate answer feedback is shown.
- Attempt summaries are saved in
useHistoryStore. - Store uses Zustand
persistmiddleware. - Metrics include score, correct answers, mode, timer, and area.
.
|-- package.json
|-- main/
| |-- main.js
| |-- preload.js
| |-- db/
| | |-- index.js
| | \-- models/
| | \-- LicenseActivation.js
| |-- ipc/
| | |-- index.js
| | |-- handlers/
| | | \-- licenseActivation.handler.js
| | \-- utils/
| | |-- ipcResponse.js
| | \-- signature.js
| |-- services/
| | |-- licenseActivation.services.js
| | \-- remoteLicense.services.js
| \-- supabase/
| \-- client.js
\-- renderer/
|-- package.json
|-- vite.config.ts
|-- src/
| |-- App.tsx
| |-- main.tsx
| |-- vite-env.d.ts
| |-- routes/routes.tsx
| |-- pages/
| \-- features/
| |-- auth/
| \-- EGEL/
\-- public/
Defined in main/db/models/LicenseActivation.js.
Fields:
id(INTEGER, PK, autoincrement)productKey(STRING, unique, not null)machineId(STRING, not null)careerId(STRING, nullable)activatedAt(DATE, default now)signature(TEXT, nullable)
Storage location:
- During Electron runtime:
app.getPath('userData')/data.sqlite - Outside Electron context: fallback to
main/db/data.sqlite
All channels return normalized envelopes from ipcResponse.js:
- success:
{ success: true, data } - error:
{ success: false, error: { code, message } }
CRUD-like:
licenseActivation:findAlllicenseActivation:createlicenseActivation:updatelicenseActivation:deletelicenseActivation:findByIdlicenseActivation:findByProductKeylicenseActivation:findFirstlicenseActivation:deleteAll
Business flows:
licenseActivation:verifyAndActivateKeylicenseActivation:verifyLocalActivation
The renderer currently consumes:
licenseActivation.findByProductKey(productKey)licenseActivation.verifyLocalActivation()licenseActivation.verifyAndActivateKey(productKey)
Create a .env file in repository root:
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your_anon_key
SIGNATURE_SECRET=replace_with_a_long_random_secretVariable usage:
SUPABASE_URL: Supabase project endpointSUPABASE_ANON_KEY: key used by main process Supabase clientSIGNATURE_SECRET: cryptographic key for activation signatures
- Node.js 18+
- npm 9+
npm run install:allThis installs root dependencies and renderer dependencies.
npm run devProcesses started:
dev:renderer: Vite dev serverdev:main: Electron app
npm run build:renderernpm run build:mainnpm run buildPipeline behavior:
- Build renderer static bundle.
- Run
electron-builder.
- Check
.envvalues (SUPABASE_URL,SUPABASE_ANON_KEY,SIGNATURE_SECRET). - Confirm remote
licensestable uses the expected columns (product_key,status,machineId,activated_at).
- Ensure
SIGNATURE_SECRETis defined. main/ipc/utils/signature.jsthrows if secret is missing.
- Verify HashRouter route paths in
renderer/src/routes/routes.tsx. - Verify first load redirects from
AuthLoaderbased onuseLicenseStorestate.
- Renderer isolation is enabled (
contextIsolation: true). - Node integration in renderer is disabled (
nodeIntegration: false). - Preload restricts renderer access to a minimal IPC API.
- Local activation integrity uses HMAC signatures.
Important caveat:
- The project currently uses Supabase anon key on client side of the main process. For stronger protection, move license validation/binding behind a secure server function with stricter policies.
- Question bank is currently hardcoded (
renderer/src/features/EGEL/services/questions.ts). - Test score logic in
TestPageis still simplified (fixed 70% strategy) instead of deriving from actual selected answers. ProtectedRouteexists but is not consistently applied to all sensitive routes in the route table.
- Enforce
ProtectedRouteon/home,/setup,/test, and/history. - Compute score from real answer evaluation using
correctAnswerIndex. - Move question bank to local DB or remote content service.
- Add automated tests for IPC handlers and state stores.
- Add telemetry/logging abstraction for production diagnostics.