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
| Surface | Technology |
|---|---|
| Web | Next.js, React, TypeScript |
| API | Express, TypeScript, JWT |
| Database | PostgreSQL, Prisma |
| Desktop | Electron, React, TypeScript, Vite |
| Browser | Manifest 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
- 01Client — issues the HTTP request
- 02API Routes — Express router, path and method only
- 03Controllers — validate input, shape the response
- 04Services — all business logic, the only layer that knows Prisma
- 05Prisma — query construction
- 06PostgreSQL — persistence
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 SQLOrigins
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.
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.