Skip to content

Architecture

daarunia edited this page Jul 11, 2026 · 1 revision

Architecture

NexTask is a single desktop application that internally behaves like a small full-stack app. This page explains the moving parts and how a request flows through them.

The big picture

NexTask runs three logical layers inside one Electron application:

flowchart LR
    subgraph Electron["Electron App"]
        subgraph Renderer["Renderer process (Chromium)"]
            UI["Vue 3 UI<br/>PrimeVue · Tailwind"]
            Stores["Pinia stores<br/>(cache + actions)"]
            Axios["Axios (api.helper)"]
        end
        subgraph Main["Main process (Node.js)"]
            Fastify["Fastify REST API<br/>:3000"]
            Prisma["Prisma Client<br/>(better-sqlite3 adapter)"]
            Store["electron-store<br/>(settings)"]
        end
        DB[("SQLite<br/>dev.db / app.db")]
    end

    UI --> Stores --> Axios
    Axios -- "HTTP :3000" --> Fastify
    Fastify --> Prisma --> DB
    UI -- "IPC (settings:get/set)" --> Store
Loading

Key idea: the Fastify server does not run in a separate process — it is started inside the Electron main process (app.whenReady). The renderer talks to it over plain HTTP on localhost:3000, exactly as if it were a remote backend. This keeps the frontend/backend boundary clean and makes the REST API reusable.

The two Electron processes

Main process — src/main

The Node.js side. On startup (src/main/main.ts) it:

  1. Creates the BrowserWindow (context isolation on, node integration off, a preload script bridging the two worlds).
  2. Sets a Content-Security-Policy header (script-src 'self').
  3. Runs database migrations then seeds.
  4. Starts the Fastify API.
  5. Registers IPC handlers for the settings store (settings:get, settings:set) and a debug message channel.

It owns everything that needs Node.js / filesystem / native modules: the HTTP API, Prisma, SQLite, and the persistent settings store.

Renderer process — src/renderer

The Chromium side — a standard Vue 3 single-page app. It has no direct database or filesystem access. It reaches data in two ways:

  • REST over HTTP for tasks and stages (via Axios → Fastify).
  • IPC for user settings (via the preload contextBridge).

The preload bridge — src/main/preload/preload.ts

Because context isolation is enabled, the renderer cannot touch Node APIs directly. The preload script uses contextBridge to expose a tiny, safe surface on window:

window.electronAPI = { sendMessage(msg) }          // debug logging channel
window.settings    = { get(key), set(key, value) } // wraps ipcRenderer.invoke('settings:get' | 'settings:set')

This is the only channel between the UI and Node.

Request lifecycle: creating a task

sequenceDiagram
    participant U as User
    participant V as Vue component<br/>(TaskDialog)
    participant P as Pinia store<br/>(useTaskStore)
    participant A as Axios (api.helper)
    participant F as Fastify route<br/>(POST /tasks)
    participant DB as Prisma → SQLite

    U->>V: Fill form, click "Save"
    V->>P: taskStore.saveTask(newTask)
    P->>A: api.post('/tasks', newTask)
    A->>F: HTTP POST :3000/tasks
    F->>DB: prisma.task.create({ data })
    DB-->>F: created Task
    F-->>A: 200 + Task JSON
    A-->>P: Task
    P->>P: update local cache (entities + allEntities)
    P-->>V: created Task
    V->>V: emit "task-saved" → board updates
Loading

The Pinia store is the client-side gateway: components never call Axios directly. Every store action performs the HTTP call and keeps a local cache in sync, so the UI can render optimistically without re-fetching. See Frontend for the caching strategy.

Data model

Two core entities plus a bookkeeping table:

erDiagram
    STAGE ||--o{ TASK : contains
    STAGE {
        int id PK
        string name
        int position
    }
    TASK {
        int id PK
        string title
        string description
        string version
        int position
        bool isHistorized
        datetime historizationDate
        int stageId FK
    }
    SEED {
        int id PK
        string name UK
        bool executed
        datetime createdAt
    }
Loading
  • A Stage is a Kanban column; a Task is a card. A task belongs to at most one stage (stageId is nullable).
  • Archiving a task sets isHistorized = true, stamps historizationDate, and clears stageId (removing it from the board).
  • Deleting a stage sets its tasks' stageId to NULL at the DB level (ON DELETE SET NULL); the app additionally archives those tasks first.
  • The Seed table (mapped to _seeds) tracks which seed scripts have run.

Full details in Database.

Technology map

Layer Technology
UI framework Vue 3 + PrimeVue + TailwindCSS
State management Pinia
Routing Vue Router (hash history)
HTTP client Axios
Desktop shell Electron
API server Fastify (+ Swagger)
ORM / DB Prisma + SQLite (better-sqlite3 adapter)
Settings persistence electron-store
Build tooling Vite, TypeScript, electron-builder

See Tech Stack for the reasoning behind each choice.

Why this design?

  • A real REST API (rather than direct DB calls from the renderer) keeps a clean separation and doubles as a documented, testable interface (Swagger UI at /docs).
  • Prisma in the main process keeps native modules and filesystem access out of the sandboxed renderer.
  • Pinia caching makes the UI feel instant while still using the API as the source of truth.

Clone this wiki locally