Skip to content

highnet/highnet-cv

Repository files navigation

highnet-cv

Write once. Publish everywhere.
A self-hosted portfolio CMS that keeps your website, CV PDF (EN + DE), GitHub README, and LinkedIn bio in perfect sync — update one field and all four outputs regenerate automatically.

Live: highnet.at (Fly.io: highnet-cv.fly.dev)


What's inside

highnet-cv/
├── apps/
│   ├── web/          # Angular 21 — public portfolio + /admin CMS
│   └── api/          # Express + TypeScript backend (serves API + Angular SPA)
├── packages/
│   ├── db/           # Drizzle ORM schema, migrations, seed
│   ├── shared/       # TypeScript types shared across apps
│   └── cv-templates/ # Handlebars HTML templates → Puppeteer A4 PDF
├── cli/              # npx highnet-cv CLI
└── fly/              # fly.toml + deploy.sh

Tech stack

Layer Tools
Frontend Angular 21, TypeScript, SCSS
Backend Express, TypeScript, Zod, Argon2id, JWT
Database PostgreSQL 17, Drizzle ORM
PDF Puppeteer + Handlebars HTML templates
Integrations Octokit (GitHub), clipboard (LinkedIn)
Infra Docker Compose (local), Fly.io (production)
Monorepo Turborepo + npm workspaces

Prerequisites

  • Node.js 20+
  • Docker Desktop (for local Postgres)
  • Angular CLI: npm i -g @angular/cli

Local development

1. Clone and install

git clone https://github.com/highnet/highnet-cv.git
cd highnet-cv
npm install

2. Configure environment

cp .env.example .env

Edit .env — required values for local dev:

DATABASE_URL=postgresql://portfolio:portfolio_dev@localhost:5432/portfolio_forge
JWT_SECRET=<any-random-32-char-hex>
ENCRYPTION_KEY=<any-random-32-char-hex>
PORT=3000
CORS_ORIGIN=http://localhost:4200

Generate secrets:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

3. Start Postgres

docker-compose up -d

4. Run migrations and seed

All commands run from the project root:

npm run db:migrate   # creates all tables via Drizzle
npm run db:seed      # seeds with portfolio data

Note: The seed requires an existing user. First visit http://localhost:4200/admin/login to create your account, then run db:seed.

5. Start all services

npm run dev
  • API → http://localhost:3000
  • Angular dev server → http://localhost:4200

Admin CMS routes

Route Description
/admin/login Login / first-run account setup
/admin/dashboard Overview and stats
/admin/profile Edit profile info
/admin/experiences Work experience
/admin/education Education history
/admin/projects Projects
/admin/skills Skills
/admin/languages Languages
/admin/cv CV preview + PDF download
/admin/integrations GitHub README push + LinkedIn copy

Public portfolio routes

Route Description
/ EN homepage
/about EN about page with CV download
/de DE homepage
/de/about DE about page

API endpoints

# Health
curl http://localhost:3000/api/health

# Public (no auth)
curl http://localhost:3000/api/public/profile
curl http://localhost:3000/api/public/projects
curl "http://localhost:3000/api/public/experiences?lang=de"

# CV PDF (cached — instant)
curl -o cv-en.pdf "http://localhost:3000/api/cv/file?lang=en"
curl -o cv-de.pdf "http://localhost:3000/api/cv/file?lang=de"

# CV PDF (on-the-fly generation — slow fallback)
curl -o cv-en.pdf "http://localhost:3000/api/cv/download?lang=en"

Production deployment (Fly.io)

The app deploys as a single Fly machine — Express serves the API and the Angular SPA as static files from the same container.

Prerequisites

  • flyctl installed and authenticated (fly auth login)

Step 1 — Create a .dockerignore

node_modules/
**/node_modules/
dist/
**/dist/
.angular/
.git/
.env
uploads/

Step 2 — Create the app

fly apps create highnet-cv --machines

Step 3 — Provision Postgres

fly postgres create \
  --name highnet-cv-db \
  --region fra \
  --initial-cluster-size 1 \
  --vm-size shared-cpu-1x \
  --volume-size 10

Step 4 — Attach Postgres

fly postgres attach highnet-cv-db --app highnet-cv

This automatically sets DATABASE_URL as a Fly secret.

Step 5 — Set remaining secrets

fly secrets set --app highnet-cv \
  JWT_SECRET=<your-secret> \
  ENCRYPTION_KEY=<your-key>

Step 6 — Create the uploads volume

fly volumes create uploads --app highnet-cv --region fra --size 1 --yes

Step 7 — Deploy

fly deploy --app highnet-cv --remote-only --config fly/fly.toml

Fly builds the Docker image remotely (Angular SPA + Express API in one container) and deploys it to Frankfurt.

Step 8 — Create admin account

Visit https://highnet.at/admin/login (or https://highnet-cv.fly.dev/admin/login) — the first visit shows a setup form to create your account.

Step 9 — Seed portfolio data

fly ssh console -a highnet-cv --command "sh -c 'cd /app/packages/db && npx tsx src/seed.ts'"

The seed is idempotent — safe to re-run to reset data.

Custom domain

fly certs create yourdomain.com --app highnet-cv
fly certs create www.yourdomain.com --app highnet-cv

Then point your DNS A/AAAA records to the Fly.io IPs shown by fly certs show yourdomain.com.


Available npm scripts

Run from the project root:

Script What it does
npm run dev Start API + Angular dev server in parallel
npm run build Production build for all packages
npm run db:generate Generate Drizzle migration files from schema
npm run db:migrate Apply pending migrations to Postgres
npm run db:seed Seed the database with portfolio data
npm run lint ESLint across all workspaces

Backend management (local + production)

Database migrations

Generate a new migration after changing the Drizzle schema:

npm run db:generate

Apply pending migrations:

# Local
npm run db:migrate

# Production (Fly.io)
fly ssh console -a highnet-cv -C "sh -c 'cd /app/packages/db && npx drizzle-kit migrate'"

Database seeding

Reset and re-seed all portfolio data (idempotent — safe to re-run):

# Local
npm run db:seed

# Production (Fly.io) — runs the seed.ts that is currently deployed in the container
fly ssh console -a highnet-cv -C "node --import tsx/esm /app/packages/db/src/seed.ts"

Adding a new project (seed → deploy workflow)

  1. Edit the seed file — add a new entry to the projects array in packages/db/src/seed.ts. Set sortOrder to the next available integer and featured: true/false.

  2. Deploy — this ships the updated seed.ts inside the Docker image:

    fly deploy --app highnet-cv --remote-only --config fly/fly.toml
  3. Re-seed on production — run the seed from inside the deployed container (it has DATABASE_URL set):

    fly ssh console -a highnet-cv -C "node --import tsx/esm /app/packages/db/src/seed.ts"

    The seed clears and re-inserts all experiences, projects, skills, and languages, so it's safe to re-run.

Important: always deploy first, then seed — the SSH command runs the seed.ts that's baked into the live container image. Seeding before deploying runs the old file and your new entry won't appear.

Why this trips you up: if you SSH in and seed before deploying, the container still has the previous image. Your local edits to seed.ts are not visible inside the container until after fly deploy completes. The seed will report "✅ Seed complete!" with no errors — but the new data won't be there because it wasn't in the file that ran.

CV cache regeneration

CVs are pre-generated and cached as PDF files. They auto-regenerate when you save data in the CMS. If needed, you can also trigger regeneration manually:

# Via admin dashboard — click "Regenerate CVs 🔄"

# Via API (authenticated)
curl -X POST https://highnet.at/api/cms/cv/regenerate \
  -H "Cookie: token=<your-jwt-token>"

# Production SSH (requires Node script)
fly ssh console -a highnet-cv -C "sh -c 'cd /app && node --import tsx/esm -e \"import { regenerateCvCache } from \\\"./apps/api/src/services/pdf.service.js\\\"; regenerateCvCache(\\\"./cv-cache\\\").then(() => console.log(\\\"done\\\"))\"'"

Production deployment

# Full rebuild + deploy
fly deploy --config fly/fly.toml --remote-only

# Check app status
fly status --app highnet-cv

# View live logs
fly logs --app highnet-cv

Environment variables

Variable Required Description
DATABASE_URL Yes PostgreSQL connection string
JWT_SECRET Yes 32+ char secret for JWT signing
ENCRYPTION_KEY Yes 32-byte hex key for GitHub token encryption
PORT No API port (default: 3000)
CORS_ORIGIN No Allowed CORS origin (default: http://localhost:4200)
UPLOAD_DIR No Profile photo upload directory (default: ./uploads)
CV_CACHE_DIR No Cached CV PDF directory (default: ./cv-cache)
NODE_ENV No Set to production to serve Angular SPA from Express

Project structure

apps/
  web/src/app/
    core/
      services/   portfolio.service.ts · auth.service.ts · i18n.service.ts
      guards/     auth.guard.ts
    admin/
      login/          /admin/login
      dashboard/      /admin/dashboard
      cv/             /admin/cv
      integrations/   /admin/integrations
      cms/
        profile/      /admin/profile
        experiences/  /admin/experiences
        education/    /admin/education
        projects/     /admin/projects
        skills/       /admin/skills
        languages/    /admin/languages
    homepage/     / and /de
    about/        /about and /de/about

  api/src/
    routes/       auth · public · cms · cv · integrations
    services/     pdf.service.ts (Puppeteer)
    middleware/   auth.middleware.ts · error.middleware.ts
    lib/          jwt.ts · crypto.ts

packages/
  db/src/
    schema/   users · profiles · experiences · education
              projects · skills · languages · integrations
    migrations/
    seed.ts
  cv-templates/  en/template.html · de/template.html (Handlebars → A4)
  shared/        TypeScript types + DTOs

License

See LICENSE.

About

A personal portfolio and CV website built with Angular, showcasing professional experience and projects. Source code to https://highnet.at/

Resources

License

Code of conduct

Stars

1 star

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors