Technical reference for the KillNode monorepo. Intended for contributors and security reviewers.
- Monorepo Overview
- Website (
website/) - Desktop (
desktop/) - CI / CD
- Security Model
- Dependency Tree (key packages)
killnode/ npm workspaces monorepo
├── package.json root — workspace definitions + top-level scripts
├── package-lock.json single lock file for all workspaces
├── website/ @killnode/website — Next.js 15
├── desktop/ @killnode/desktop — Electron 33
├── docs/ documentation
└── .github/workflows/ CI (ci.yml) + releases (release-desktop.yml)
npm workspaces hoist shared dependencies to the root node_modules. Both workspaces reference the root node_modules via symlinks. The --workspace flag targets a specific package: npm run build --workspace=desktop.
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router) |
| Styling | Tailwind CSS 3, shadcn-style components |
| Database ORM | Prisma 6 |
| Database | PostgreSQL (Neon cloud) |
| Auth | jose JWT — HTTP-only session cookie |
| Deployment | Vercel |
| Analytics | @vercel/analytics (no PII) |
website/src/app/
├── layout.tsx root layout (font, global CSS, analytics)
├── page.tsx landing page (hero, download widget, features)
├── not-found.tsx global 404
├── globals.css Tailwind base styles
├── blog/
│ ├── page.tsx blog index (all published posts)
│ └── [slug]/page.tsx individual post (static-rendered at build time)
└── admin/
├── login/
│ ├── page.tsx server component wrapper
│ └── admin-login-client.tsx client component (form, fetch)
└── (protected)/ route group — layout checks session cookie
├── layout.tsx auth guard (redirects to /admin/login if no valid session)
├── page.tsx admin dashboard (post list)
└── posts/
├── new/page.tsx create post form
└── [slug]/edit/page.tsx edit post form
| Route | Method | Purpose |
|---|---|---|
/api/auth/login |
POST | Validate credentials, issue JWT session cookie |
/api/auth/logout |
POST | Clear session cookie |
/api/posts |
GET | List all posts (public) |
/api/posts |
POST | Create post (authenticated) |
/api/posts/[slug] |
GET | Get single post (public) |
/api/posts/[slug] |
PUT | Update post (authenticated) |
/api/posts/[slug] |
DELETE | Delete post (authenticated) |
/api/releases/latest |
GET | Proxy GitHub Releases API → returns latest release metadata |
joseis used to sign and verify JWTs.- The session cookie is
HttpOnly,Secure(in production),SameSite=Lax. - Session validation on protected routes is done in two places:
- Edge middleware (
src/middleware.ts): lightweight check usingauth-edge.ts— runs on Vercel Edge Network before the route even loads. - Route-group layout (
admin/(protected)/layout.tsx): server-side re-check for defence in depth.
- Edge middleware (
- Credentials are compared using constant-time comparison to prevent timing attacks.
// website/prisma/schema.prisma
model Post {
id String @id @default(cuid())
slug String @unique
title String
excerpt String @default("")
content String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}DATABASE_URL: pooled connection (pgBouncer) — used for all runtime queries.DIRECT_URL: direct (non-pooled) connection — used by Prisma Migrate for advisory-lock operations. Not required during Vercel builds (migrations run separately).- Generated client output:
website/src/generated/prisma/. - Seed data:
website/data/posts.json→ loaded bywebsite/prisma/seed.ts.
Configuration is split across two files:
| File | Purpose |
|---|---|
vercel.json (root) |
Build command, output directory, security headers, env |
website/vercel.json |
Additional website-specific overrides |
.vercelignore (root) |
Excludes desktop/release/ and other large artifacts |
Build command on Vercel:
cd website && npx prisma generate && next buildMigrations are not run during Vercel builds — they must be applied manually against Neon before deploying schema changes.
| Layer | Technology |
|---|---|
| Framework | Electron 33.4.11 (Node.js 22 / Chromium 130) |
| Bundler | electron-vite 2 (Vite 5 for renderer, Rollup for main/preload) |
| Language | TypeScript 5 throughout |
| Database ORM | Prisma 6 |
| Database | SQLite (via Prisma sqlite provider) |
| Tor | Spawned child process (tor binary from Tor Expert Bundle) |
| Tor control | Native TCP socket to control port 9051 (cookie auth) |
| Proxy | proxy-chain (HTTP), hardened SOCKS5 gateway |
| Packaging | electron-builder 25 |
Electron uses a multi-process architecture:
┌─────────────────────────────────────────┐
│ Main Process (Node.js 22) │
│ index.ts │
│ ├── tor-manager.ts Tor lifecycle │
│ ├── tor-readiness.ts SOCKS probe │
│ ├── local-proxy.ts HTTP bridge │
│ ├── socks5-gateway.ts SOCKS5 ingress │
│ ├── torrent-service.ts WebTorrent │
│ ├── proxy-controller.ts Orchestrates │
│ ├── killswitch-orchestrator.ts │
│ ├── network-killswitch.ts OS hooks │
│ ├── obfuscation-bridge.ts SS/V2Ray │
│ ├── settings-service.ts SQLite KV │
│ ├── process-registry.ts Child mgmt │
│ ├── sql-bootstrap.ts DB init │
│ ├── onion-link.ts .onion helpers │
│ ├── tray-manager.ts System tray │
│ └── prisma.ts Prisma client │
│ │
│ IPC (contextBridge) ←──────────────→ │
│ │
│ Preload Process │
│ preload/index.ts allow-listed │
│ ipcRenderer │
│ │
│ Renderer Process (Chromium) │
│ renderer/src/main.ts Three-card UI │
│ renderer/src/style.css UI styles │
└─────────────────────────────────────────┘
The preload script exposes a strictly allow-listed window.kn API via contextBridge.exposeInMainWorld. The renderer can only call channels that are explicitly listed in preload/index.ts.
Renderer → Main (invoke):
kn:tor:start Start Tor
kn:tor:stop Stop Tor
kn:tor:status Current Tor status
kn:tor:newident SIGNAL NEWNYM (new circuits)
kn:tor:bootstrap Bootstrap progress + circuit count
kn:proxy:status Proxy port info and session state
kn:killswitch Fire neural killswitch
kn:restore-hint Recovery command hint
kn:settings:get Read all settings
kn:settings:set Write partial settings update
kn:dialog:tor Native file picker for tor binary
Main → Renderer (on):
kn:toast Show a toast notification
kn:tor:status-push Push Tor running status change
kn:dead-man-fired Dead-man timer has fired
kn:dirty-shutdown Unclean exit detected on launch
No nodeIntegration is enabled in the renderer. All Node.js operations run in the main process behind the IPC boundary.
| File | Responsibility |
|---|---|
tor-manager.ts |
Writes torrc.killnode, spawns tor, handles process events |
tor-readiness.ts |
Polls the configured SOCKS port until it accepts TCP |
local-proxy.ts |
proxy-chain HTTP bridge: :9742 → socks5://127.0.0.1:<tor> |
socks5-gateway.ts |
Minimal SOCKS5 server: :9741 → Tor SOCKS for outbound TCP |
torrent-service.ts |
WebTorrent client lifecycle, magnet ingestion, seeding, telemetry |
proxy-controller.ts |
Orchestrates the activation sequence (Tor → readiness → proxies → session proxy) |
killswitch-orchestrator.ts |
Ordered teardown: torrent → proxies → obfuscation → Tor → network severance |
network-killswitch.ts |
Platform-specific network severance (netsh, nmcli/ip, networksetup) |
obfuscation-bridge.ts |
Spawns Shadowsocks and/or V2Ray child processes |
settings-service.ts |
Key/value store on top of the Setting Prisma model |
process-registry.ts |
Tracks all managed child processes; bulk-kills on shutdown |
sql-bootstrap.ts |
Creates database file and applies DDL on first launch (no migrations) |
tray-manager.ts |
System tray icon, context menu, window show/hide |
prisma.ts |
Singleton PrismaClient with SQLite database path wired to Electron userData |
// desktop/prisma/schema.prisma
model Setting {
key String @id
value String
}- The database file lives in Electron's
userDatadirectory (killnode.db). - No migrations — DDL is applied from
sql-bootstrap.tson first launch. The schema and bootstrap SQL must be kept in sync manually. - Generated client output:
desktop/src/main/generated/prisma/. - The Vite plugin
copyDesktopPrismaClientinelectron.vite.config.tscopiessrc/main/generated→out/main/generatedon every build so the packaged app can resolve the client. asarUnpackindesktop/package.jsonunpacks@prisma/**and.prisma/**from the ASAR archive so the native Prisma query engine is accessible at runtime.
npm run dev:desktop
└── prisma generate --schema prisma/schema.prisma
└── electron-vite dev
├── Vite dev server for renderer (HMR)
├── Rollup watch for main (CJS bundle → out/main/)
└── Rollup watch for preload (CJS bundle → out/preload/)
npm run build:desktop
└── prisma generate
└── electron-vite build
├── Rollup main → out/main/index.js
├── Rollup preload → out/preload/index.js
└── Vite renderer → out/renderer/
└── copyDesktopPrismaClient plugin: src/main/generated → out/main/generated
npm run package:desktop
└── node scripts/download-tor.mjs
├── Detects platform (win32 / linux / darwin)
├── Downloads Tor Expert Bundle v15.0.9 from torproject.org mirror chain
├── Verifies SHA256 against known checksum — aborts on mismatch
└── Extracts to desktop/resources/tor/ (gitignored)
└── npm run build:desktop
└── electron-builder --publish never
├── Windows: NSIS installer + ZIP (includes resources/tor/tor.exe)
├── Linux: AppImage + .deb (includes resources/tor/tor)
└── Output: desktop/release/
Runs on push/PR to main and develop.
Website job (ubuntu-latest):
- Spins up
postgres:16service container. - Installs dependencies (
npm ci). - Runs
prisma generate + migrate deploy + db seedagainst the local Postgres. - Runs ESLint (
npm run lint:web). - Runs
next build.
Desktop job (matrix: ubuntu-latest, windows-latest, macos-latest):
- Installs dependencies.
- Typechecks (
tsc --noEmit— afterprisma generate). - Builds (
electron-vite build).
No packaging or release artifacts are produced by CI; it is a pure validation gate.
Triggered on any v* tag push.
Matrix (currently Windows + Linux; macOS disabled until Phase 2):
- Checks out code.
- Pins Python 3.11 (for
node-gypcompatibility withdistutils). - Installs Node.js 20 + dependencies.
- Runs
npm run packageinsidedesktop/(--publish neverprevents auto-publish). - Uploads
desktop/release/*to the GitHub Release viasoftprops/action-gh-release@v2.
Cutting a release:
git tag v0.2.0 -m "Alpha 2"
git push origin v0.2.0| Setting | Value | Why |
|---|---|---|
contextIsolation |
true |
Renderer JS cannot access Node.js globals |
nodeIntegration |
false |
No require in renderer |
sandbox |
Electron default (enabled) | Renderer runs in Chromium sandbox |
Only channels explicitly listed in preload/index.ts can be invoked by the renderer. There is no wildcard forwarding of ipcRenderer.invoke.
Served by Vercel via vercel.json headers:
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com;
connect-src 'self' https://api.github.com https://vitals.vercel-insights.com;
frame-ancestors 'none';
See USAGE.md — Threat Model for what is and is not protected.
| Package | Version | Purpose | License |
|---|---|---|---|
electron |
33.4.11 | Desktop runtime | MIT |
electron-vite |
^2.3.0 | Dev server + bundler | MIT |
electron-builder |
^25.1.8 | Installer packaging | MIT |
webtorrent |
^2.5.1 | BitTorrent client (ESM-only) | MIT |
magnet-uri |
^7.0.5 | Magnet link parser (ESM-only) | MIT |
proxy-chain |
^2.4.1 | HTTP → SOCKS proxy bridge | Apache-2.0 |
socks-proxy-agent |
^8.0.4 | SOCKS agent for Node.js http |
MIT |
socks |
^2.8.3 | SOCKS protocol primitives | MIT |
@prisma/client |
^6.3.0 | Database ORM client | Apache-2.0 |
prisma |
^6.3.0 | Prisma CLI + schema tooling | Apache-2.0 |
next |
^15.0.3 | Web framework | MIT |
jose |
^5.9.6 | JWT signing / verification | MIT |
tailwindcss |
^3.4.14 | Utility-first CSS | MIT |
Full dependency graph: see package-lock.json at the repository root.