Documentation

Architecture

Four codebases, one API and one PostgreSQL database. How a foreground window becomes a row.

Four packages, one API, one database. Clients never talk to each other.

The stack

SurfaceTechnology
WebNext.js, React, TypeScript
APIExpress, TypeScript, JWT
DatabasePostgreSQL, Prisma
DesktopElectron, React, TypeScript, Vite
BrowserManifest V3, TypeScript

Server layers

Routes map paths to controllers, controllers validate and shape, services hold the logic and are the only layer that imports Prisma.

One request, top to bottom

  1. Client — issues the HTTP request
    01
  2. API Routes — Express router, path and method only
    02
  3. Controllers — validate input, shape the response
    03
  4. Services — all business logic, the only layer that knows Prisma
    04
  5. Prisma — query construction
    05
  6. PostgreSQL — persistence
    06

How an activity gets stored

1. Electron polls the foreground window        → appName
2. Extension resolves the active tab            → domain + title
3. Client POSTs an Activity                     → { source, appName | domain }
4. Service applies per-user category overrides   → category
5. Service inserts the row                      → Activity.create
6. Dashboard reads back with a range query      → aggregates in SQL

Origins

CORS is allow-listed with credentials enabled. Three origins pass: the web client, any chrome-extension:// origin, and the desktop app, which in production loads from file:// and arrives as the literal string null.

server/src/app.ts
if (origin === env.clientUrl) return callback(null, true);
if (origin.startsWith("chrome-extension://")) return callback(null, true);
if (origin === "null") return callback(null, true);
callback(new Error("Not allowed by CORS"));

How errors are returned

Handlers throw an AppError with a status and optional data. One error handler converts it to JSON. Unexpected errors are logged and reduced to a generic 500, so internals never reach a client.