An NX monorepo containing two frontend applications, a shared REST API, and a set of platform components.
NodeVault (nodevault.cloud) — a privacy information portal covering three practical paths to reducing your digital footprint: de-Googling your phone with GrapheneOS, replacing cloud subscriptions with a self-hosted UmbrelOS home server, and protecting your home network from IoT surveillance.
Nick Champion (nickchampion.me) — a personal profile and CV site showcasing engineering background, technical expertise, and project work.
Node.js API server built on Koa with OpenAPI v3 validation via openapi-backend. Runs locally with tsx watch and deploys to Fly.io as a Docker container (nodevault-api, lhr region).
Stack: Node.js · Koa · openapi-backend · RavenDB · TypeScript
Dev server: pnpm run api → http://api.nodevault.local:9002
Magic-link, no passwords:
POST /auth/login— looks up user by email, generates an encrypted token stored as aLoginTokendocument (10-minute TTL via RavenDB@expires), fetches the rendered login email from the Nuxt app, and sends it via Resend.- User clicks the link →
POST /auth/verify— decrypts the code, validates theLoginToken, marks it used, and returns a signed JWT + user/account payload.
export const authLogin: ApiHandler = async (context): Promise<Response> => {
const { email } = context.event.payload as LoginRequestSchema
// ...
return context.event.response.ok()
}Handlers are registered by name in apps/api/handlers/index.ts — the key must exactly match the operationId in the OpenAPI schema. The middy middleware lifecycle auto-commits the RavenDB session after each handler.
Koa → OpenAPI route match + schema validation
→ InboundEvent + Context created (RavenDB Session attached)
→ middy middleware:
before: logging, timer
handler: ApiHandler
after: commit session, set response headers
error: normalise to StandardResponse
→ Koa response
Nuxt 4 (compatibilityVersion: 4) SSR app deployed to Cloudflare Workers via the cloudflare_module preset. All pages server-render by default; no prerendering.
Stack: Nuxt 4 · Vue 3 · Nuxt UI · Pinia · Tailwind CSS · TypeScript
Dev server: pnpm run app → http://www.nodevault.local:9001
Deploy: pnpm run app:build → Wrangler → Cloudflare Workers (nodevault worker)
| Section | Path | Description |
|---|---|---|
| Privacy Phones | /phones |
GrapheneOS — de-Googling your phone, compatible devices, privacy app stack |
| Home Server | /umbrelos |
UmbrelOS self-hosting — hardware, app directory, replacing cloud subscriptions |
| Privacy Router | /privacy-router |
DNS blocking, WireGuard VPN, VLAN isolation for home networks |
| Blog | /blog |
Articles and guides |
| Contact | /company/contact |
Get in touch form |
| About | /company/about |
About NodeVault |
| Layout | Used for |
|---|---|
default |
All public-facing pages — sticky header, footer |
admin |
Admin section — full-width header, left sidebar nav |
email |
Email templates — branded email card shell (header + footer), no Nuxt chrome injected into inline styles |
Email templates are Nuxt pages under /emails/* using the email layout. The API calls renderEmail(appUrl, '/emails/login', params) which fetches the SSR-rendered HTML and passes it to Resend. Query params supply the template variables (e.g. name, code).
| Path | Purpose |
|---|---|
app/stores/auth-store.ts |
Pinia store — JWT tokens, expiry, apiOptions() |
app/composables/useApiClient.ts |
Returns a typed NodeVaultApiClient bound to auth tokens |
app/composables/useConfig.ts |
Runtime config access |
Nuxt 4 SSR app deployed to Cloudflare Workers. Light-only UI (sky/slate colour scheme) with forced light mode — dark mode is disabled at the CSS level regardless of system preference.
Stack: Nuxt 4 · Vue 3 · Nuxt UI · Tailwind CSS · TypeScript
Dev server: pnpm run nickchampion → http://www.nickchampion.local:9003
Deploy: Cloudflare Workers (nickchampion worker)
| Route | Description |
|---|---|
/ |
Landing page — intro, key strengths, technical expertise, current project, recent roles |
/cv |
Full CV — career history, education, notable achievements |
/nodevault |
NodeVault project showcase — what it is, tech stack, engineering highlights |
/contact |
Contact form — name, email, optional phone, message; posts to /comms/contact API endpoint |
- No auth — fully public, no admin section
- Contact form submits to the same
/comms/contactAPI endpoint as NodeVault, withinterests: ['other']injected silently (required by the shared schema) - Uses
PhoneInputcomponent copied from NodeVault, backed byCountriesdomain model - Dark mode disabled:
@variant darkredefined tonever-darkin CSS, plus.darkCSS variable block overridden to light values inapp/assets/css/main.css
| Package alias | Path | Purpose |
|---|---|---|
@nodevault/platform.components.api |
components/api |
Koa server, OpenAPI routing, middy middleware, handler types |
@nodevault/platform.components.nodevault.server |
components/nodevault/server |
Server config, domain models, RavenDB indexes |
@nodevault/platform.components.nodevault.client |
components/nodevault/client |
Client runtime config, NodeVaultApiClient, typed request methods |
@nodevault/platform.components.nodevault.openapi |
components/nodevault/openapi |
OpenAPI document composition, models, request/response schemas |
@nodevault/platform.components.configuration |
components/configuration |
Config builder (build<T>()) |
@nodevault/platform.components.context |
components/context |
Context, InboundEvent, Response, Log, middy wrappers |
@nodevault/platform.components.domain |
components/domain |
Domain models (User, Account, LoginToken, Contact), types, geo data |
@nodevault/platform.components.ravendb |
components/ravendb |
Session wrapper, document store helpers, search utilities |
@nodevault/platform.components.search |
components/search |
Search/query builders |
@nodevault/platform.components.utils |
components/utils |
Pure utilities — date, string, math (no Node.js-specific APIs) |
@nodevault/platform.components.utils.server |
components/utils-server |
Server-only utilities — crypto, encoding, JWT |
@nodevault/platform.integrations.resend |
integrations/resend |
Resend email client — createResendClient, sendEmail |
@nodevault/platform.integrations.cloudflare |
integrations/cloudflare |
Cloudflare Workers helpers |
# Install dependencies
pnpm install
# Start servers (separate terminals)
pnpm run api # API on :9002
pnpm run app # NodeVault Nuxt on :9001
pnpm run nickchampion # Nick Champion Nuxt on :9003
# Type check everything
npx tsc --noEmit
# Run tests
npx vitest
# Lint with autofix
pnpm run lint
# Regenerate OpenAPI client types from schemas
pnpm run schemasAdd to /etc/hosts:
127.0.0.1 api.nodevault.local
127.0.0.1 www.nodevault.local
127.0.0.1 www.nickchampion.local
Server config is passed as a base64-encoded JSON string in the NODEVAULT environment variable. Local overrides are read from the path in NODEVAULT_OVERRIDES. See components/configuration/server/configuration.ts for the full config schema.
| App | Platform | Command |
|---|---|---|
apps/api |
Fly.io (Docker, lhr) |
fly deploy from apps/api/ |
apps/nodevault |
Cloudflare Workers | pnpm run app:build then wrangler deploy from apps/nodevault/ |
apps/nickchampion |
Cloudflare Workers | build then wrangler deploy from apps/nickchampion/ |