A modern CV management system built on Payload CMS 3 and Next.js 16, designed for companies to create, manage, and export professional CVs.
- Multi-language support — German and English with localized content
- Flexible skill system — Organize skills in hierarchical groups with customizable proficiency levels
- PDF export — Generate professional PDFs with customizable branding and layout
- PostgreSQL — Migrations run automatically on startup
- S3 storage — Media files in any S3-compatible storage, Garage included in the provided setups
- OAuth integration — Single sign-on with your identity provider
- Multi-tenant — Manage CVs across multiple organizations
The docker-compose directory runs the tegonal/cv-manager image with PostgreSQL, Garage (S3 storage for media files) and Caddy (HTTPS). You need Docker with Compose. To work on the code instead, see Development.
curl -fsSL https://github.com/tegonal/cv-manager/archive/refs/heads/main.tar.gz \
| tar -xz --strip-components=1 cv-manager-main/docker-compose
cd docker-composeThe stack does not start until the secrets in .env are set. This fills in the empty ones and leaves existing values untouched:
for key in PAYLOAD_SECRET POSTGRES_PASSWORD S3_SECRET_ACCESS_KEY GARAGE_RPC_SECRET; do
sed -i.bak "s/^$key=\$/$key=$(openssl rand -hex 32)/" .env
done
sed -i.bak "s/^S3_ACCESS_KEY_ID=\$/S3_ACCESS_KEY_ID=GK$(openssl rand -hex 12)/" .env
rm .env.bakKeep a copy of .env. The database is created with POSTGRES_PASSWORD, changing it in .env later locks the application out.
docker compose up -dOpen https://localhost/admin and create the first user, it becomes the administrator.
- The browser warns about the certificate: Caddy signs
localhostwith its own local authority. - Ports 80 and 443 must be free.
- The image is built for amd64, on Apple Silicon it runs emulated and slower.
Point a DNS name to the server and open ports 80 and 443, Caddy then gets a certificate from Let's Encrypt.
- Set
PUBLIC_URL=https://cv.example.comin.envand replacelocalhostwithcv.example.comin theCaddyfile. - Optionally set up SMTP for password reset emails and OAuth in
.env. - Run
docker compose up -d, open https://cv.example.com/admin and create the first user right away: until a user exists, anyone who can reach the instance can create it. - Once HTTPS works, enable the
Strict-Transport-Securityheader in theCaddyfileand rundocker compose restart proxy.
Back up the postgres-data and garage-data volumes and .env. A database dump:
docker compose exec -T postgres pg_dump -U postgres -Fc cv-manager > cv-manager.pgdumpTo upgrade, set a new CV_MANAGER_VERSION as described in the docker compose README. Database migrations run automatically on startup.
| Variable | Description |
|---|---|
PAYLOAD_SECRET |
Signs login tokens, at least 32 random characters in production (required) |
PUBLIC_URL |
URL under which the instance is reachable, defaults to http://localhost:3000 |
DATABASE_URI |
PostgreSQL connection string, e.g. postgres://user:pass@host:5432/db (required) |
Media files are stored in S3-compatible storage (required):
S3_ENDPOINT=https://s3.example.com
S3_BUCKET=cv-manager-media
S3_ACCESS_KEY_ID=your-access-key
S3_SECRET_ACCESS_KEY=your-secret-key
S3_REGION=us-east-1 # defaults to garageThe provided docker compose setup includes Garage as S3-compatible storage, and so do the local development services.
Configure SMTP for password recovery emails:
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your-smtp-user
SMTP_PASS=your-smtp-password
SMTP_FROM_ADDRESS=noreply@example.com
SMTP_FROM_NAME=CV Manager # optional, the sender namePort 465 uses TLS from the start, other ports upgrade to TLS when the server offers it. SMTP_USER and SMTP_PASS can be left out for servers that accept mail without login.
Without SMTP, no emails are sent and password recovery does not work (Payload logs a warning at startup). Super admins can set a new password for a user in the admin panel.
Enable single sign-on with your identity provider:
OAUTH_ENABLED=true
OAUTH_CLIENT_ID=your-client-id
OAUTH_CLIENT_SECRET=your-client-secret
OAUTH_TOKEN_ENDPOINT=https://auth.example.com/token
OAUTH_AUTHORIZE_ENDPOINT=https://auth.example.com/authorize
OAUTH_USERINFO_ENDPOINT=https://auth.example.com/userinfo
# Users of these email domains get an account on their first login
OAUTH_ALLOWED_EMAIL_DOMAINS=example.comWithout OAUTH_ALLOWED_EMAIL_DOMAINS, only users that already have an account can log in with OAuth. New accounts join the default organisation with the user role. Logins with an email the provider reports as not verified (email_verified: false) are rejected.
Configure PDF appearance through the admin panel (Settings) without code changes:
| Setting | Where | Options |
|---|---|---|
| Footer | Company Info | Name, address, city, website |
| Logo | PDF Style | Upload (SVG, PNG, JPG), width (mm), position (left/right), margins, first page only/all pages |
| Typography | PDF Style | Font family (Rubik, Open Sans, Lato, Roboto, Merriweather, Playfair Display) |
| Colors | PDF Style | Primary color (borders, highlights), secondary color (skill indicators) |
| Skill Levels | PDF Style | Display as text, dots, or progress bars |
| Page Layout | PDF Style | Format (A4/Letter), margins in mm, separate first page margins, first page centered or left-aligned |
Layouts beyond these settings are code: fork the repository, adapt the react-pdf template in src/payload/plugins/cv-pdf-generator/templates/default/index.tsx and build your own image. Shared utilities in templates/lib/ provide date formatting, Lexical rich-text rendering, and Tailwind CSS helpers.
Set up these entities before creating CVs:
| Entity | Purpose |
|---|---|
| Organizations | Tenants, users and their data belong to an organization |
| Skill Groups | Categories like "Programming Languages", "Frameworks" |
| Skills | Individual skills within groups |
| Levels | Proficiency levels for skills and languages (e.g. Junior, Expert) |
| Languages | Spoken languages, the proficiency is set per CV |
| Companies | Employers and clients referenced in the work experience |
| Projects | Shared project references |
Each CV contains:
| Section | Content |
|---|---|
| Profile | Personal info, photo, introduction, contact details, links |
| Skills | Languages, highlights, hierarchical skill groups with levels, other skills |
| Education | Highlights, degrees, certifications, courses |
| Experience | Highlights and project history with descriptions |
Skills can be nested to match different career profiles:
Frontend Developer:
Frontend Technologies
├── React — Expert
├── Vue.js — Senior
└── Angular — Junior
Full Stack Developer:
Full Stack Development
├── Backend Frameworks — Expert
│ └── Spring, Node.js, Django
├── Databases — Senior
│ └── PostgreSQL, MongoDB
└── Frontend — Advanced
└── React, Vue.js
Requires Node.js 24 and Docker.
nvm use
yarn install
yarn run services:start # Start Postgres, Garage (S3) and Mailpit
cp .env.example .env # Preconfigured for the local services
yarn run devMails are caught by Mailpit at http://localhost:8025. On first start, an admin user admin@test.com / admin and demo data are created.
yarn run dev also writes its output to .logs/dev.log (overwritten on each start, not committed).
yarn run check # Lint, format (with fixes), and type-check
yarn run check:ci # The same without fixes, as run by CIAfter schema changes, generate a migration:
yarn run migrate:createRun one-off scripts with the Payload config and .env loaded, e.g. to query data with the Local API:
yarn payload run path/to/script.ts