About Notez
Notez is a desktop application for organizing notes, documents, and files in a hierarchical tree structure. It is built as an Electron app backed by a centralized REST API server. This page documents the full technical architecture.
1. Architecture Overview
Notez is a three-tier client-server application organized as an npm workspaces monorepo with three packages:
| Package | Role |
|---|---|
@notez/client |
Electron + React desktop application (renderer = React SPA, main process = Node.js) |
@notez/server |
Express REST API handling authentication, authorization, business logic, and file I/O |
@notez/shared |
Shared TypeScript types, constants, and API route definitions used by both client and server |
The Electron client is not served by the Express server. It is a standalone desktop application that communicates with the API over HTTPS. In production, the API runs at notezapi.qstatecode.com behind a reverse proxy that terminates TLS.
2. Technology Stack
Server
| Component | Technology |
|---|---|
| Language | TypeScript, compiled with tsc, dev via ts-node-dev |
| Framework | Express 4.18 |
| Database | Microsoft SQL Server 2019+ via the mssql npm package |
| ORM | None — raw parameterized SQL queries via mssql connection pools |
| Validation | Zod 3.22 for all request bodies, params, and query strings |
| Auth | jsonwebtoken (JWT, HS256) |
| Password hashing | bcrypt with 12 salt rounds |
| Logging | Winston 3.11 (file + console transports, structured JSON) |
| File uploads | Multer 1.4 (memory storage) |
| HTTP security | Helmet 7.1, CORS, express-rate-limit 7.1, compression |
| HTML sanitization | DOMPurify 3.0 (server-side via jsdom) |
| Testing | Vitest + Supertest |
Client
| Component | Technology |
|---|---|
| UI framework | React 18.2 |
| Routing | React Router DOM 6.22 (HashRouter for Electron file:// compatibility) |
| State management | Zustand 4.5 (4 stores: auth, project, tree, UI) |
| Server state | TanStack React Query 5.24 |
| Rich text editor | TipTap 3.17 (built on ProseMirror) |
| TipTap extensions | StarterKit, Image, Placeholder, Table, TableRow, TableCell, TableHeader |
| Drag and drop | @dnd-kit/core 6.1 + @dnd-kit/sortable 8.0 |
| Tree virtualization | @tanstack/react-virtual 3.1 |
| CSS | Tailwind CSS 3.4 with @tailwindcss/typography plugin |
| Icons | Lucide React |
| Notifications | Sonner 1.4 |
| Document preview | mammoth 1.6 (.docx), pdfjs-dist 4.0 (PDF) |
| Build tool | Vite 7.3 with vite-plugin-electron |
| Testing | Vitest + Testing Library |
Electron Shell
| Component | Detail |
|---|---|
| Electron version | 40.0.0 |
| Build tool | electron-builder 23.x |
| Context isolation | Enabled (contextIsolation: true, nodeIntegration: false) |
| Preload bridge | Exposes electronAPI: platform info, window controls (minimize, maximize, close) |
| App ID | com.notez.app |
| Targets | Windows (NSIS x64), macOS (DMG x64 + arm64), Linux (AppImage x64) |
3. Data Model & Schema
The database runs on Microsoft SQL Server 2019+. All primary keys are UNIQUEIDENTIFIERs (GUIDs) generated with NEWID(). All major entities use a soft-delete pattern via a nullable deleted_at column — records are never physically removed through the application.
| Table | Purpose | Key Columns |
|---|---|---|
| Users | User accounts | id, username (case-insensitive unique), password_hash |
| Permissions | Permission definitions | id, name (create, read, update, delete) |
| Settings | App configuration (key-value) | key, value, description |
| Projects | Top-level containers | id, owner_id (FK Users), name |
| ProjectUsers | Project membership (junction) | project_id + user_id (composite PK) |
| ProjectUserPermissions | Per-user, per-project permissions | project_id + user_id + permission_id (composite PK) |
| Nodes | Tree folders/items | id, project_id, parent_id (self-FK, NULL = root), name, sort_order |
| TextObjects | Rich text documents | id, node_id (FK), name, stored_filename, file_path, file_size |
| FileAttachments | Binary file attachments | id, node_id (FK), name, original_filename, stored_filename, file_type (MIME), file_size |
| Images | Inline images for rich text | id, text_object_id (FK), original_filename, stored_filename, file_size |
| Tags | Project-scoped labels | id, project_id (FK), label (lowercase, unique per project) |
| NodeTags | Node-tag junction (many-to-many) | node_id + tag_id (composite PK, cascade delete) |
Entity Relationships
- Users own zero or more Projects (via
owner_id) and can be members of others (via ProjectUsers). - Each project membership carries granular Permissions (create, read, update, delete) through ProjectUserPermissions.
- A Project contains a tree of Nodes. Nodes self-reference via
parent_idto form unlimited-depth hierarchy. - Each Node holds zero or more TextObjects (rich text documents) and FileAttachments.
- Each TextObject can contain zero or more Images (inline in the rich text HTML).
- Tags are scoped to a project and linked to nodes via NodeTags (many-to-many).
Indexes
Filtered indexes on name/label columns (where deleted_at IS NULL) optimize search queries: idx_nodes_name, idx_textobjects_name, idx_attachments_name, idx_tags_label.
4. Data Flow & API
All client-server communication is over REST (JSON over HTTPS). There are no WebSocket connections. File uploads use multipart/form-data via Multer. Every endpoint lives under the /api/v1 prefix.
Client-Side Request Pipeline
- UI components dispatch actions through React Query mutation/query hooks.
- Hooks call the ApiClient singleton, which wraps
fetch()and automatically attaches theAuthorization: Bearer <JWT>header. - On 401 Unauthorized responses, the ApiClient triggers the auth store’s logout flow automatically.
- React Query manages caching, deduplication, and invalidation. Mutations invalidate related queries to keep the UI in sync.
Server-Side Request Pipeline
- Rate limiter — global 100 req/min/IP; auth endpoints 10 req/min/IP.
- Helmet — sets security headers (CSP, HSTS, X-Content-Type-Options, etc.).
- CORS — validates origin against an allowlist (Electron
nullorigin,notez://protocol, configurable domains). - Body parser — JSON with a 10 MB limit; Multer for multipart uploads.
- Auth middleware — verifies JWT signature and expiry, extracts user identity and project permissions from the token payload.
- Permission middleware — checks the token’s embedded permissions for the target project (no database round-trip required).
- Zod validation — validates request body, params, and query against typed schemas.
- Controller — executes business logic, calls the database and storage service.
- Error handler — catches all exceptions and returns structured JSON error responses.
API Endpoints
| Group | Endpoints | Description |
|---|---|---|
| Auth | POST register, login, logout, refresh |
Account creation, JWT issuance and refresh. Auth endpoints have a stricter rate limit (10/min). |
| Projects | GET list, POST create, PUT update, DELETE delete/leave |
Project CRUD. Owners can delete; members can leave. Requires appropriate permissions. |
| Tree | GET full tree |
Returns the entire node hierarchy for a project, including text objects, attachments, tags, and node-tag mappings in a single response. |
| Nodes | CRUD + move + reorder + batch delete | Create, read, rename, soft-delete, drag-to-move (validates against circular references), reorder within parent. |
| Text Objects | CRUD + content read/write + move + reorder + batch delete | Metadata and HTML content are separate endpoints. Content is sanitized with DOMPurify on every write. |
| Attachments | Upload + CRUD + stream/download + move + reorder + batch delete | Multipart upload with magic-byte validation. Stream and download endpoints serve file bytes directly. |
| Images | POST upload, GET stream |
For inline images embedded in rich text. JPEG, PNG, GIF, WebP only. SVG blocked (XSS risk). |
| Tags | CRUD + node-tag association | Project-scoped labels. Add/remove tags from nodes. Labels stored lowercase. |
| Search | GET search |
Full-text search across nodes, text objects, attachments, and tags with optional tag filtering and subtree scoping. |
| Health | GET /health |
Health check endpoint, exempt from rate limiting. |
5. Security
Authentication
- JWT tokens signed with HS256. Token expiry is configurable via the
Settingstable (default: 24 hours). - The JWT payload includes user ID, username, and the full list of project memberships with their permissions. This allows authorization checks without a database round-trip on each request.
- Tokens are stored in the client’s
localStorageunder the keynotez_auth_token. The client checksexpon load and triggers a refresh when within 5 minutes of expiry. - A
POST /api/v1/auth/refreshendpoint re-queries the database for current user/project/permission data and issues a fresh token.
Password Security
- Passwords are hashed with bcrypt using 12 salt rounds before storage. Plaintext passwords are never persisted or logged.
Transport Security
- In production, HTTPS is terminated at the reverse proxy (IIS with URL Rewrite or nginx) using TLS certificates.
- The Electron app’s Content Security Policy restricts
connect-srctohttps://notezapi.qstatecode.comin production builds. - The database connection uses
encrypt: truefor the TDS (Tabular Data Stream) protocol.
HTTP Security Headers
- Helmet sets:
X-Content-Type-Options: nosniff,X-Frame-Options,Strict-Transport-Security, Content Security Policy, and others. - Full CSP is enforced in production; a relaxed policy is used during development.
Rate Limiting
- Global: 100 requests per minute per IP.
- Auth endpoints: 10 requests per minute per IP (brute-force protection).
- Health check endpoints are exempt. Exceeded limits return
429 Too Many Requests.
Input Validation & Sanitization
- All request inputs (body, params, query) are validated with Zod schemas before reaching controllers. Invalid requests are rejected with structured error responses.
- Rich text HTML content is sanitized both client-side and server-side with DOMPurify using a strict allowlist of HTML tags and attributes.
- All SQL queries use parameterized inputs via the
mssqllibrary’srequest.input()method — no string concatenation of user data into queries.
File Upload Security
- Magic byte validation: the server reads the first bytes of every uploaded file and verifies that the actual file content matches the claimed MIME type.
- Extension whitelist: only configured extensions are accepted (default:
pdf, docx, txt, jpg, jpeg, png, gif, webp). Configurable via the Settings table. - SVG explicitly blocked to prevent stored XSS attacks.
- Size limit: 32 MB per file (configurable).
Path Traversal Prevention
- The
StorageServiceresolves and normalizes all file paths, rejecting any that resolve outside the configured storage root directory.
Electron Security
- Context isolation is enabled: the renderer process cannot directly access Node.js APIs.
- Node integration is disabled.
- A preload script exposes a minimal
electronAPIbridge with only platform info and window control functions.
CORS
- The server validates request origins against an allowlist: Electron’s
nullorigin, thenotez://custom protocol, localhost ports in development, and any domains specified in theCORS_ORIGINSenvironment variable. - Unrecognized origins are rejected in production.
Authorization
- A CRUD permission model (create, read, update, delete) is enforced per-user, per-project.
- Permissions are stored in the
ProjectUserPermissionstable and embedded in the JWT, so therequirePermission()middleware can authorize requests from the token alone. - Only the project owner can delete a project entirely. Other members can only leave.
6. File Storage
All files are stored on the local filesystem of the server machine. There is no S3 or cloud object storage. The storage root is configurable via the storage_root key in the Settings table (default: C:\NotezStorage on Windows, or /app/storage in Docker).
| Content Type | Directory | Naming |
|---|---|---|
| Rich text documents | {storage_root}/text-objects/ |
{name}_{guid}.html |
| File attachments | {storage_root}/attachments/ |
{name}_{guid}.{ext} |
| Inline images | {storage_root}/images/ |
{uuid}.{ext} |
The StorageService is a singleton that handles all file I/O operations. It provides path traversal prevention, DOMPurify sanitization of HTML before writing, magic byte validation, extension whitelist enforcement, file size limits, and idempotent delete operations.
Metadata (filenames, paths, sizes, MIME types) is stored in the database. The actual file bytes live on disk. Soft-deleting a record in the database does not remove the file from disk.
Data Storage Notice: All data is currently stored on QState Code's servers. A future update will provide a configurable server endpoint so users can point the client at their own infrastructure and retain full control over where data is stored.
7. Feature Details
Rich Text Editor
The editor is built on TipTap 3.17 (a headless ProseMirror wrapper). It supports bold, italic, strikethrough, inline code, headings (H1–H6), blockquotes, bullet lists, ordered lists, code blocks, horizontal rules, and undo/redo (100 levels of history). Tables with resizable columns are available via the TipTap Table extension. Inline images are uploaded to the server and referenced by URL — no base64 data URIs are stored in the document HTML.
Content is saved with Ctrl+S. The UI tracks unsaved changes and warns before navigating away.
Hierarchical Tree
Nodes form an unlimited-depth tree via a self-referencing parent_id foreign key. The full tree for a project is fetched in a single API call and assembled server-side into a nested structure. The client stores it in a Zustand store as a Map<GUID, TreeNode> for O(1) lookups.
The tree supports: expand/collapse, expand-all/collapse-all, range selection (Shift+click), multi-selection (Ctrl+click), keyboard navigation, inline rename editing, and drag-and-drop reordering and moving via @dnd-kit. The tree is virtualized with @tanstack/react-virtual for performance with large node counts.
File Attachments & Previews
Any node can have file attachments uploaded via multipart form data. The server validates uploads with magic byte checks and an extension whitelist. Files are stored on the local filesystem under {storage_root}/attachments/.
The client provides built-in previews for: PDF (rendered page-by-page with pdfjs-dist), Word .docx (converted to HTML via mammoth), images (native rendering), and plain text. Unsupported types show a download fallback.
Tagging & Search
Tags are project-scoped labels stored in lowercase. They can be attached to any node through the NodeTags junction table. The search system runs SQL LIKE pattern matching (case-insensitive via MSSQL collation) across nodes, text objects, attachments, and tags simultaneously using UNION ALL queries.
Search supports scoping to the entire project, a specific subtree (via recursive CTE), or filtering by selected tags. Results include breadcrumb paths showing the full ancestor chain for each match.
Multi-Project & Permissions
Users can create multiple projects and be invited to others. Each membership carries independent CRUD permissions (create, read, update, delete) via the ProjectUserPermissions table. The project owner always has full access and is the only user who can delete the project. Other members can leave voluntarily.
8. Deployment & Distribution
Server Deployment
The production server targets Windows Server 2019+ or Linux. Prerequisites are Node.js 20+ LTS and MSSQL Server 2019+. The deployment process is:
- Create the MSSQL database and run the schema and migration scripts.
- Configure environment variables:
NODE_ENV,PORT,DB_SERVER,DB_NAME,DB_USER,DB_PASSWORD,JWT_SECRET. - Create storage directories (
text-objects/,attachments/,images/) and set thestorage_rootvalue in the Settings table. - Build the server with
npm run build:server(TypeScript compilation). - Start with
node dist/server.js.
Process Management
Two options are supported:
- PM2:
pm2 start dist/server.js --name notez-apiwithpm2 saveandpm2 startup. - Windows Service: An install script uses the
node-windowspackage to register the API as a Windows Service named “Notez API”.
Docker
A docker-compose.yml is provided with two services:
- api — the Express server on port 3000.
- mssql — Microsoft SQL Server 2019 on port 1433.
Docker volumes persist file storage (notez-storage) and database data (mssql-data).
HTTPS
TLS is terminated at the reverse proxy (IIS URL Rewrite or nginx) rather than in the Node.js process. The production API runs at notezapi.qstatecode.com.
Desktop App Distribution
| Platform | Format | Build Command |
|---|---|---|
| Windows | NSIS installer (.exe), x64 | npm run build:win |
| macOS | DMG, x64 + arm64 | npm run build:mac |
| Linux | AppImage, x64 | npm run build:linux |
Code Quality
- Linting & formatting: ESLint + Prettier, enforced via Husky pre-commit hooks with lint-staged.
- Unit tests: Vitest + Supertest (server), Vitest + Testing Library (client).
- E2E tests: Playwright.
Open Source — Coming Soon
The full Notez source code will be released as open source. Once published, you will be able to self-host the entire stack — server, database, and storage — on your own infrastructure.