https://github.com/nickchampion/node-vault
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.
https://github.com/nickchampion/node-vault
grapheneos networking phones privacy umbrelos
Last synced: about 1 month ago
JSON representation
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.
- Host: GitHub
- URL: https://github.com/nickchampion/node-vault
- Owner: nickchampion
- Created: 2026-05-02T09:58:19.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-22T16:38:36.000Z (about 1 month ago)
- Last Synced: 2026-05-22T16:58:12.162Z (about 1 month ago)
- Topics: grapheneos, networking, phones, privacy, umbrelos
- Language: TypeScript
- Homepage: https://www.nodvault.cloud
- Size: 652 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# NodeVault
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.
---
## Apps
### `apps/api` — REST API
Node.js API server built on Koa with OpenAPI v3 validation via `openapi-backend`. Runs locally with `tsx watch` and deploys to [Fly.io](https://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`
#### Auth flow
Magic-link, no passwords:
1. `POST /auth/login` — looks up user by email, generates an encrypted token stored as a `LoginToken` document (10-minute TTL via RavenDB `@expires`), fetches the rendered login email from the Nuxt app, and sends it via Resend.
2. User clicks the link → `POST /auth/verify` — decrypts the code, validates the `LoginToken`, marks it used, and returns a signed JWT + user/account payload.
#### Handler pattern
```typescript
export const authLogin: ApiHandler = async (context): Promise => {
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.
#### Request lifecycle
```
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
```
---
### `apps/nodevault` — NodeVault frontend
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)
#### Content areas
| 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 |
#### Layouts
| 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 rendering
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`).
#### Key composables & stores
| 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 |
---
### `apps/nickchampion` — Personal profile site
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)
#### Pages
| 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 |
#### Notes
- No auth — fully public, no admin section
- Contact form submits to the same `/comms/contact` API endpoint as NodeVault, with `interests: ['other']` injected silently (required by the shared schema)
- Uses `PhoneInput` component copied from NodeVault, backed by `Countries` domain model
- Dark mode disabled: `@variant dark` redefined to `never-dark` in CSS, plus `.dark` CSS variable block overridden to light values in `app/assets/css/main.css`
---
## Components (shared libraries)
| 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()`) |
| `@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 |
---
## Development
```bash
# 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 schemas
```
### Local hosts
Add to `/etc/hosts`:
```
127.0.0.1 api.nodevault.local
127.0.0.1 www.nodevault.local
127.0.0.1 www.nickchampion.local
```
### Configuration
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.
---
## Deployment
| 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/` |