Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b46bda4e6 | ||
|
|
df81d0342f | ||
|
|
4825eed22c | ||
|
|
b0711351f7 | ||
|
|
b7e4c86f4e | ||
|
|
c25c068d20 | ||
|
|
d217d5dc4f | ||
|
|
1ed80140b5 | ||
|
|
28ae7714c8 | ||
|
|
9d70bc4fd3 | ||
|
|
e5a51013c1 | ||
|
|
eeaac9a86f | ||
|
|
269dbc600f | ||
|
|
9b29a44429 | ||
|
|
e9fa161148 | ||
|
|
299d6d4878 | ||
|
|
ea63ae6e72 | ||
|
|
4d50cd9b71 | ||
|
|
8b3f3bcc35 | ||
|
|
f73a5117a1 | ||
|
|
2d1e13b9b5 | ||
|
|
b6c274eeb6 | ||
|
|
4b78a7c9dd | ||
|
|
87e2f2f391 | ||
|
|
f237c42093 | ||
|
|
a80f86e99e | ||
|
|
eb62ed99ce | ||
|
|
45fc32323f | ||
|
|
f1d69da019 | ||
|
|
9d131fdb99 | ||
|
|
29ef6307e0 | ||
|
|
c3c771002f | ||
|
|
554903b818 | ||
|
|
6b71ecd7c6 | ||
|
|
833b8343ac | ||
|
|
8e060890d3 | ||
|
|
abf0fc74e5 | ||
|
|
6242c8c182 | ||
|
|
15327d74d8 | ||
|
|
90c34ca572 | ||
|
|
2b8fa9c7e8 | ||
|
|
2f7aabcfe9 | ||
|
|
ef6d054598 | ||
|
|
201d707aea | ||
|
|
d8b0902ac9 | ||
|
|
01c75bd796 | ||
|
|
0a609306a6 | ||
|
|
5bdc6de862 | ||
|
|
cc01fb9418 | ||
|
|
5e8e1349bc | ||
|
|
ddbb71fb78 | ||
|
|
3289453448 | ||
|
|
611af8ee72 | ||
|
|
69b96a592d | ||
|
|
3151f6a9cc | ||
|
|
726968288c | ||
|
|
ad086f337a | ||
|
|
66d09820c3 | ||
|
|
5ec73e9234 | ||
|
|
17569d0658 | ||
|
|
14ea464c0a | ||
|
|
da9a3c0b12 | ||
|
|
f5a9ffb776 | ||
|
|
19181b79c8 | ||
|
|
1509678578 | ||
|
|
c4d5dd4a3c | ||
|
|
ca734ca547 | ||
|
|
8d347f5162 | ||
|
|
4d368ab6f6 | ||
|
|
cabacdc44b | ||
|
|
71dac2021d | ||
|
|
a8d8d0e340 | ||
|
|
97274d1c80 | ||
|
|
aa12fcbd36 | ||
|
|
3d1c2d1fb6 | ||
|
|
f6fcbdcad0 | ||
|
|
30e6ba0809 | ||
|
|
3cb0fc9f30 | ||
|
|
5f578c3327 | ||
|
|
e56402058a | ||
|
|
19ae21e797 | ||
|
|
b89a31d4ae | ||
|
|
07e06a6ed4 | ||
|
|
89102d6612 | ||
|
|
feda173b3b | ||
|
|
02f812ce96 | ||
|
|
7250bed701 | ||
|
|
2b11b1a234 | ||
|
|
18e8aadf18 | ||
|
|
4840a292df | ||
|
|
76ca1995ca | ||
|
|
0f333b1f24 | ||
|
|
1f8eaa423f | ||
|
|
4e4d3670a2 | ||
|
|
cd9a6a80e3 | ||
|
|
429618d5f8 | ||
|
|
1e814a77cc | ||
|
|
41b0c57725 | ||
|
|
470822f4a9 | ||
|
|
946bf9ec38 | ||
|
|
49585940f7 | ||
|
|
9257d62216 | ||
|
|
3a9d2e7652 | ||
|
|
4b2e3c0ded | ||
|
|
6978df4c3c | ||
|
|
1f13189ec3 | ||
|
|
c08e8081fe | ||
|
|
d8c9b5a936 | ||
|
|
fa823e1296 | ||
|
|
de89ab957b | ||
|
|
96920495d0 | ||
|
|
92044de6b8 | ||
|
|
8e8ef300ec | ||
|
|
982220430e | ||
|
|
3f55c24e36 | ||
|
|
0c65612368 | ||
|
|
3071b7dd37 | ||
|
|
5daac24e73 | ||
|
|
bcd2828305 | ||
|
|
b5f4e9af46 | ||
|
|
92f6a6a16b | ||
|
|
019a20d027 | ||
|
|
1378feaf6a | ||
|
|
22221e6fae | ||
|
|
d773ff5391 | ||
|
|
be84f0cca6 | ||
|
|
1419c16d04 | ||
|
|
89f209014e | ||
|
|
21aec46763 | ||
|
|
333b5e50c4 | ||
|
|
e94550b7b2 | ||
|
|
dcdf8398da | ||
|
|
503f610ff6 | ||
|
|
c875cc858d | ||
|
|
403384944c | ||
|
|
4e73a81d4b | ||
|
|
ed74fb67f2 | ||
|
|
895af548a0 | ||
|
|
a32450ab22 | ||
|
|
a4fd7f8452 | ||
|
|
8214f2a910 | ||
|
|
06d54a9060 | ||
|
|
d801effbce | ||
|
|
6658412f8b | ||
|
|
11cbeb27f8 | ||
|
|
6db9fee823 | ||
|
|
35e2daa807 | ||
|
|
0b61ddda2d | ||
|
|
0045bbd785 | ||
|
|
a10954ed14 | ||
|
|
0bc53b9c2a | ||
|
|
2aeed6c58f | ||
|
|
36a9e27c93 | ||
|
|
36a0094631 | ||
|
|
5d73998f82 | ||
|
|
3cb6f763ca | ||
|
|
a438130784 | ||
|
|
c72d6205e0 | ||
|
|
ce16f81dbf | ||
|
|
c786f58ab8 | ||
|
|
ec19386317 | ||
|
|
5576184f1b | ||
|
|
ebb40664f2 | ||
|
|
e5ad94e620 | ||
|
|
e5934bf94b | ||
|
|
5cfd7e00bb | ||
|
|
649bfefd06 | ||
|
|
0e96b9bfae | ||
|
|
4b87ab6fe9 | ||
|
|
ff2986b3f8 | ||
|
|
8d75537153 | ||
|
|
8ffce2a264 | ||
|
|
083466202e | ||
|
|
7dff245b1a | ||
|
|
272f0795d1 | ||
|
|
22ebb7b8eb | ||
|
|
f6ab720d73 | ||
|
|
a735580540 | ||
|
|
3bcd480345 | ||
|
|
63684ce9a0 | ||
|
|
fc4678ae98 | ||
|
|
f87575958a | ||
|
|
6b051dbf60 | ||
|
|
fb8917cf40 | ||
|
|
8c674fe510 | ||
|
|
f5c4487bb9 | ||
|
|
588cee17fb | ||
|
|
ea1ac92a56 | ||
|
|
12c415e505 | ||
|
|
2c292e3bad | ||
|
|
dc3d3c2971 | ||
|
|
c86b049c92 | ||
|
|
b0ca3ddfab | ||
|
|
59fc84d975 | ||
|
|
13f817d057 | ||
|
|
b50e11e4b4 | ||
|
|
d56d834245 | ||
|
|
06d23ac98e | ||
|
|
6ec5a409d0 | ||
|
|
7fd0a66eac | ||
|
|
52184112b1 | ||
|
|
89473ac4e7 | ||
|
|
2f66afdc87 | ||
|
|
16cbad2028 | ||
|
|
2f49075d89 | ||
|
|
8771f2716a | ||
|
|
4d72a953dd | ||
|
|
b5ea68af5b | ||
|
|
499346e7ba | ||
|
|
81f4a3e148 | ||
|
|
7fd54c4259 | ||
|
|
fda2d384e6 | ||
|
|
270ccf746c | ||
|
|
6b51d1bbaa | ||
|
|
a0e17f0c27 | ||
|
|
70064be7de | ||
|
|
332286f9ce | ||
|
|
7584412fcb | ||
|
|
b3c342b029 | ||
|
|
cf6db1a0ee | ||
|
|
2ac5c9137c | ||
|
|
8bafefe1b8 | ||
|
|
5a42b776ff | ||
|
|
dfe53629bc | ||
|
|
fc6690d047 | ||
|
|
b3904750b8 | ||
|
|
63ea01b219 | ||
|
|
4345f48dcc | ||
|
|
7f8869649f | ||
|
|
06777e8285 | ||
|
|
0907bddf75 | ||
|
|
8cf225a3e7 | ||
|
|
71a931fdee | ||
|
|
aa2109f4bd | ||
|
|
c2edf9651a | ||
|
|
88e5c3e87e | ||
|
|
e2d593327a | ||
|
|
104047f672 | ||
|
|
9c3faf1edf | ||
|
|
1dc5bf21e0 | ||
|
|
8bc4f3de84 | ||
|
|
6af3b32302 | ||
|
|
dd517627c1 | ||
|
|
05d5824131 | ||
|
|
655138c0ea | ||
|
|
7209eecaba | ||
|
|
3d36849508 | ||
|
|
2e42146102 | ||
|
|
ab05f2533f | ||
|
|
53f796aef7 | ||
|
|
09ea72cb2f | ||
|
|
84cc897fb4 | ||
|
|
d3cc2b8011 | ||
|
|
f4d56195c4 | ||
|
|
ce33a71a43 | ||
|
|
90ae8511b5 | ||
|
|
f41d53cf36 | ||
|
|
e16437aeed | ||
|
|
c2d942d3f2 | ||
|
|
cc5a90b3ca | ||
|
|
2942c5a329 | ||
|
|
d214ea8319 | ||
|
|
7a80b13db8 | ||
|
|
d55e179890 | ||
|
|
33563a4cf8 | ||
|
|
fe21523934 | ||
|
|
9840613e0a | ||
|
|
80c3e80dba | ||
|
|
c6ab0700b7 | ||
|
|
6d23773569 | ||
|
|
f5bc16944f | ||
|
|
004b9bd8c4 | ||
|
|
4b7fa5a303 | ||
|
|
9efc9653a7 | ||
|
|
0ff51a82ee | ||
|
|
d874ea4f83 | ||
|
|
5688c97ee0 | ||
|
|
3cbf953ad2 | ||
|
|
4f61e921cd | ||
|
|
f903e6099b | ||
|
|
f3a6c0db6b | ||
|
|
5880078bfc | ||
|
|
6689232f7a | ||
|
|
29cc5be9b3 | ||
|
|
9dd0611ccc | ||
|
|
b0c8017839 | ||
|
|
891828af8e | ||
|
|
32466c758c | ||
|
|
3b11316982 | ||
|
|
b87e5dd023 | ||
|
|
7e5597271b | ||
|
|
3d1334f7d9 | ||
|
|
5f5c2e1a52 | ||
|
|
ce8d273c79 |
@@ -0,0 +1,10 @@
|
||||
FROM mcr.microsoft.com/devcontainers/typescript-node:24
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
EXPOSE 3000
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "Reactive Resume",
|
||||
"service": "reactive_resume",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"workspaceFolder": "/workspace",
|
||||
|
||||
"forwardPorts": [3000, 4000, 5432, 8333],
|
||||
"portsAttributes": {
|
||||
"3000": { "label": "Reactive Resume", "onAutoForward": "openBrowser" },
|
||||
"4000": { "label": "Browserless (Printer)" },
|
||||
"5432": { "label": "PostgreSQL" },
|
||||
"8333": { "label": "SeaweedFS (S3)" }
|
||||
},
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["biomejs.biome", "bradlc.vscode-tailwindcss", "lokalise.i18n-ally"],
|
||||
"settings": {
|
||||
"biome.enabled": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.biome": "explicit",
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.organizeImports.biome": "explicit"
|
||||
},
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"postCreateCommand": "corepack enable && pnpm install"
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
services:
|
||||
reactive_resume:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
volumes:
|
||||
- ..:/workspace:cached
|
||||
command: sleep infinity
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
browserless:
|
||||
condition: service_started
|
||||
seaweedfs:
|
||||
condition: service_healthy
|
||||
seaweedfs_create_bucket:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
TZ: Etc/UTC
|
||||
APP_URL: http://localhost:3000
|
||||
PRINTER_APP_URL: http://reactive_resume:3000
|
||||
PRINTER_ENDPOINT: ws://browserless:3000?token=1234567890
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
|
||||
AUTH_SECRET: change-me-to-a-secure-secret-key-in-production
|
||||
S3_ACCESS_KEY_ID: seaweedfs
|
||||
S3_SECRET_ACCESS_KEY: seaweedfs
|
||||
S3_REGION: us-east-1
|
||||
S3_ENDPOINT: http://seaweedfs:8333
|
||||
S3_BUCKET: reactive-resume
|
||||
S3_FORCE_PATH_STYLE: "true"
|
||||
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "postgres", "-d", "postgres"]
|
||||
start_period: 10s
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
browserless:
|
||||
image: ghcr.io/browserless/chromium:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
QUEUED: "10"
|
||||
HEALTH: "true"
|
||||
CONCURRENT: "5"
|
||||
TOKEN: "1234567890"
|
||||
healthcheck:
|
||||
test:
|
||||
["CMD", "curl", "-f", "http://localhost:3000/pressure?token=1234567890"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
seaweedfs:
|
||||
image: chrislusf/seaweedfs:latest
|
||||
restart: unless-stopped
|
||||
command: server -s3 -filer -dir=/data -ip=0.0.0.0
|
||||
environment:
|
||||
AWS_ACCESS_KEY_ID: seaweedfs
|
||||
AWS_SECRET_ACCESS_KEY: seaweedfs
|
||||
volumes:
|
||||
- seaweedfs_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:8888"]
|
||||
start_period: 10s
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
seaweedfs_create_bucket:
|
||||
image: quay.io/minio/mc:latest
|
||||
restart: on-failure
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
until mc alias set seaweedfs http://seaweedfs:8333 seaweedfs seaweedfs; do
|
||||
echo 'Waiting for SeaweedFS...';
|
||||
sleep 2;
|
||||
done;
|
||||
mc mb seaweedfs/reactive-resume --ignore-existing;
|
||||
"
|
||||
depends_on:
|
||||
seaweedfs:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
seaweedfs_data:
|
||||
@@ -3,21 +3,14 @@ TZ="Etc/UTC"
|
||||
APP_URL="http://localhost:3000"
|
||||
|
||||
# Optional, uses APP_URL by default
|
||||
# This can be set to a different URL (like http://host.docker.internal:3000 or http://{docker_service}:3000)
|
||||
# to let the browser navigate to a non-public instance of Reactive Resume
|
||||
# PLEASE READ: This should be set to an internal URL (like http://host.docker.internal:3000 or http://{docker_service}:3000)
|
||||
# to let the browser navigate to a non-public instance of Reactive Resume.
|
||||
# This is required when the printer service is running inside Docker, and cannot reach the app via the APP URL,
|
||||
# which is usually when the APP_URL is localhost or a local network IP/hostname.
|
||||
PRINTER_APP_URL="http://host.docker.internal:3000"
|
||||
|
||||
# Note: set this to `http://host.docker.internal:3000` if your `gotenberg` service is running on docker, but Reactive Resume is running outside of Docker.
|
||||
|
||||
# --- Printer ---
|
||||
GOTENBERG_ENDPOINT="http://localhost:4000"
|
||||
|
||||
# Gotenberg Authentication (Optional)
|
||||
# TIP: It's safest to avoid exposing your Gotenberg instance to the public internet; connect via a private Docker network instead.
|
||||
# However, if Gotenberg needs to be hosted remotely from Reactive Resume, use these credentials to enable basic authentication for security.
|
||||
# For setup details and security best practices, see: https://gotenberg.dev/docs/configuration#api:~:text=API%5FENABLE%5FBASIC%5FAUTH
|
||||
# GOTENBERG_USERNAME=""
|
||||
# GOTENBERG_PASSWORD=""
|
||||
PRINTER_ENDPOINT="ws://localhost:4000?token=1234567890"
|
||||
|
||||
# --- Database (PostgreSQL) ---
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
|
||||
@@ -68,7 +61,14 @@ S3_FORCE_PATH_STYLE="true"
|
||||
FLAG_DEBUG_PRINTER="false"
|
||||
|
||||
# This flag disables new signups, both on the web app and the server.
|
||||
FLAG_DISABLE_SIGNUP="false"
|
||||
FLAG_DISABLE_SIGNUPS="false"
|
||||
|
||||
# This flag disables email/password login. Disables email verification, forgot password, and reset password flows. Users can still sign up via social auth (Google/GitHub/Custom OAuth), unless FLAG_DISABLE_SIGNUPS is also set to true.
|
||||
FLAG_DISABLE_EMAIL_AUTH="false"
|
||||
|
||||
# This flag disables the image processing.
|
||||
# This is useful if you are using a machine with limited resources, like a Raspberry Pi.
|
||||
FLAG_DISABLE_IMAGE_PROCESSING="false"
|
||||
|
||||
# --- Others ---
|
||||
# Google Cloud API Key (optional)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
locales/*.po linguist-generated=true
|
||||
@@ -20,8 +20,8 @@
|
||||
"url": "https://rxresu.me"
|
||||
},
|
||||
"repositoryUrl": {
|
||||
"url": "https://github.com/AmruthPillai/Reactive-Resume",
|
||||
"wellKnown": "https://github.com/AmruthPillai/Reactive-Resume/blob/main/.github/.well-known/funding-manifest-urls"
|
||||
"url": "https://github.com/amruthpillai/reactive-resume",
|
||||
"wellKnown": "https://github.com/amruthpillai/reactive-resume/blob/main/.github/.well-known/funding-manifest-urls"
|
||||
},
|
||||
"licenses": ["spdx:MIT"],
|
||||
"tags": ["data", "design", "productivity", "resume-builder"]
|
||||
|
||||
@@ -3,7 +3,7 @@ name: ✨ Feature Request
|
||||
description: Suggest an feature or idea that you would like to see in Reactive Resume
|
||||
|
||||
title: "[Feature] <title>"
|
||||
labels: [feature, v5, needs triage]
|
||||
labels: [enhancement, v5, needs triage]
|
||||
assignees: "AmruthPillai"
|
||||
|
||||
body:
|
||||
|
||||
@@ -2,9 +2,6 @@ name: Build Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: ["main"]
|
||||
tags: ["v*.*.*"]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -67,8 +64,7 @@ jobs:
|
||||
ghcr.io/${{ env.IMAGE }}
|
||||
docker.io/${{ env.IMAGE }}
|
||||
tags: |
|
||||
type=sha,format=short,suffix=-${{ matrix.arch }}
|
||||
type=raw,value=v${{ steps.version.outputs.version }}-${{ matrix.arch }}
|
||||
type=sha,prefix=sha-,suffix=-${{ matrix.arch }}
|
||||
|
||||
- name: Build and Push by Digest
|
||||
id: build
|
||||
@@ -81,6 +77,7 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
cache-from: type=gha,scope=${{ env.IMAGE }}-${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=${{ env.IMAGE }}-${{ matrix.arch }}
|
||||
|
||||
@@ -111,7 +108,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: package.json
|
||||
sparse-checkout-cone-mode: false
|
||||
@@ -143,6 +140,16 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Parse version components
|
||||
id: semver
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||
MINOR=$(echo "$VERSION" | cut -d. -f2)
|
||||
|
||||
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
|
||||
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
@@ -151,25 +158,53 @@ jobs:
|
||||
ghcr.io/${{ env.IMAGE }}
|
||||
docker.io/${{ env.IMAGE }}
|
||||
tags: |
|
||||
type=sha,format=short
|
||||
type=sha,prefix=sha-
|
||||
type=raw,value=latest
|
||||
type=raw,value=v${{ steps.version.outputs.version }}
|
||||
type=raw,value=v${{ steps.semver.outputs.major }}.${{ steps.semver.outputs.minor }}
|
||||
type=raw,value=v${{ steps.semver.outputs.major }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
id: manifest
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker buildx imagetools create \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
--annotation "index:org.opencontainers.image.licenses=MIT" \
|
||||
--annotation "index:org.opencontainers.image.title=Reactive Resume" \
|
||||
--annotation "index:org.opencontainers.image.description=A free and open-source resume builder." \
|
||||
--annotation "index:org.opencontainers.image.vendor=Amruth Pillai" \
|
||||
--annotation "index:org.opencontainers.image.url=https://rxresu.me" \
|
||||
--annotation "index:org.opencontainers.image.documentation=https://docs.rxresu.me" \
|
||||
--annotation "index:org.opencontainers.image.source=https://github.com/amruthpillai/reactive-resume" \
|
||||
--annotation "index:org.opencontainers.image.version=${{ steps.version.outputs.version }}" \
|
||||
$(printf 'ghcr.io/${{ env.IMAGE }}@sha256:%s ' *) \
|
||||
$(printf 'docker.io/${{ env.IMAGE }}@sha256:%s ' *)
|
||||
|
||||
# Get the digest of the multi-arch manifest
|
||||
GHCR_DIGEST=$(docker buildx imagetools inspect ghcr.io/${{ env.IMAGE }}:v${{ steps.version.outputs.version }} --format '{{json .Manifest.Digest}}' | tr -d '"')
|
||||
DOCKER_DIGEST=$(docker buildx imagetools inspect docker.io/${{ env.IMAGE }}:v${{ steps.version.outputs.version }} --format '{{json .Manifest.Digest}}' | tr -d '"')
|
||||
echo "ghcr_digest=$GHCR_DIGEST" >> "$GITHUB_OUTPUT"
|
||||
echo "docker_digest=$DOCKER_DIGEST" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3
|
||||
|
||||
- name: Sign images with Cosign
|
||||
run: |
|
||||
# Sign GHCR image
|
||||
cosign sign --yes ghcr.io/${{ env.IMAGE }}@${{ steps.manifest.outputs.ghcr_digest }}
|
||||
|
||||
# Sign Docker Hub image
|
||||
cosign sign --yes docker.io/${{ env.IMAGE }}@${{ steps.manifest.outputs.docker_digest }}
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ghcr.io/${{ env.IMAGE }}:latest
|
||||
docker buildx imagetools inspect ghcr.io/${{ env.IMAGE }}:v${{ steps.version.outputs.version }}
|
||||
docker buildx imagetools inspect docker.io/${{ env.IMAGE }}:v${{ steps.version.outputs.version }}
|
||||
|
||||
- name: Redeploy Service
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
- name: Redeploy Stack
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
|
||||
@@ -5,6 +5,7 @@ dist
|
||||
.output
|
||||
.vercel
|
||||
.cursor
|
||||
TODO.md
|
||||
coverage
|
||||
.netlify
|
||||
.DS_Store
|
||||
@@ -12,4 +13,7 @@ coverage
|
||||
node_modules
|
||||
!.env.example
|
||||
public/sw.js
|
||||
public/workbox-*.js
|
||||
public/sw.js.map
|
||||
scripts/**/*.json
|
||||
public/workbox-*.js
|
||||
public/workbox-*.js.map
|
||||
@@ -1,48 +0,0 @@
|
||||
stages:
|
||||
- build
|
||||
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay2
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
|
||||
build-and-push:
|
||||
stage: build
|
||||
image: docker:24
|
||||
services:
|
||||
- docker:24-dind
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_PIPELINE_SOURCE == "web"
|
||||
before_script:
|
||||
- apk add --no-cache jq
|
||||
- echo "$CI_REGISTRY_PASSWORD" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
|
||||
- |
|
||||
export VERSION=$(jq -r .version package.json)
|
||||
echo "VERSION=$VERSION" >> build.env
|
||||
script:
|
||||
- source build.env
|
||||
- |
|
||||
SHORT_SHA=$CI_COMMIT_SHORT_SHA
|
||||
PRIMARY_TAG="$CI_REGISTRY_IMAGE:$SHORT_SHA"
|
||||
|
||||
# Build the image
|
||||
docker build -f ./Dockerfile -t $PRIMARY_TAG .
|
||||
|
||||
# Tag with additional tags
|
||||
docker tag $PRIMARY_TAG $CI_REGISTRY_IMAGE:latest
|
||||
if [ -n "$VERSION" ] && [ "$VERSION" != "null" ]; then
|
||||
docker tag $PRIMARY_TAG $CI_REGISTRY_IMAGE:v$VERSION
|
||||
fi
|
||||
|
||||
# Push all tags (only if not a merge request)
|
||||
if [ "$CI_PIPELINE_SOURCE" != "merge_request_event" ]; then
|
||||
docker push $PRIMARY_TAG
|
||||
docker push $CI_REGISTRY_IMAGE:latest
|
||||
if [ -n "$VERSION" ] && [ "$VERSION" != "null" ]; then
|
||||
docker push $CI_REGISTRY_IMAGE:v$VERSION
|
||||
fi
|
||||
fi
|
||||
after_script:
|
||||
- docker logout $CI_REGISTRY || true
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const nextPackages = ["@monaco-editor/react"];
|
||||
|
||||
const betaPackages = ["drizzle-orm", "drizzle-kit", "@better-auth/core", "@better-auth/passkey", "better-auth"];
|
||||
const betaPackages = ["vite", "drizzle-orm", "drizzle-kit"];
|
||||
|
||||
/** @type {import('npm-check-updates').RunOptions} */
|
||||
module.exports = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"[mdx]": {
|
||||
"editor.defaultFormatter": "unifiedjs.vscode-mdx"
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"biome.enabled": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
@@ -9,28 +9,25 @@
|
||||
"source.organizeImports.biome": "explicit"
|
||||
},
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"eslint.enable": false,
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
},
|
||||
"files.readonlyInclude": {
|
||||
"locales/*.po": true,
|
||||
"pnpm-lock.yaml": true,
|
||||
"**/routeTree.gen.ts": true
|
||||
"**/routeTree.gen.ts": true,
|
||||
"pnpm-lock.yaml": true
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"locales/*.po": true,
|
||||
"pnpm-lock.yaml": true,
|
||||
"**/routeTree.gen.ts": true
|
||||
"**/routeTree.gen.ts": true,
|
||||
"locales/**.po": true,
|
||||
"pnpm-lock.yaml": true
|
||||
},
|
||||
"i18n-ally.enabledParsers": ["po"],
|
||||
"i18n-ally.localesPaths": ["locales"],
|
||||
"i18n-ally.sourceLanguage": "en-US",
|
||||
"prettier.enable": false,
|
||||
"search.exclude": {
|
||||
"locales/*.po": true,
|
||||
"pnpm-lock.yaml": true,
|
||||
"**/routeTree.gen.ts": true
|
||||
"**/routeTree.gen.ts": true,
|
||||
"locales/**.po": true,
|
||||
"pnpm-lock.yaml": true
|
||||
},
|
||||
"tailwindCSS.classFunctions": ["cn", "cva"],
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Cursor Cloud specific instructions
|
||||
|
||||
### Overview
|
||||
|
||||
Reactive Resume is a single-package full-stack TypeScript app (not a monorepo) built with TanStack Start (React 19, Vite, Nitro). It serves both frontend and API on port 3000.
|
||||
|
||||
### Infrastructure services
|
||||
|
||||
Before running the dev server, Docker must be running with at least PostgreSQL. Start services via `compose.dev.yml`:
|
||||
|
||||
```bash
|
||||
sudo dockerd &>/var/log/dockerd.log &
|
||||
sudo docker compose -f compose.dev.yml up -d postgres browserless
|
||||
```
|
||||
|
||||
- **PostgreSQL** (port 5432) — required. The app auto-runs Drizzle migrations on startup via a Nitro plugin.
|
||||
- **Browserless** (port 4000) — required for PDF export. Maps container port 3000 to host port 4000.
|
||||
|
||||
### Environment variables
|
||||
|
||||
Copy `.env.example` to `.env` if not present. Key notes for local dev:
|
||||
|
||||
- `APP_URL` — local dev server origin on port 3000.
|
||||
- `PRINTER_APP_URL` — must use the Docker bridge gateway IP (not localhost) so the Browserless container can reach the app on the host. Get the IP with: `sudo docker network inspect reactive_resume_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'`
|
||||
- `PRINTER_ENDPOINT` — websocket URL to Browserless on host port 4000 with token `1234567890`.
|
||||
- `DATABASE_URL` — PostgreSQL connection using `postgres:postgres` credentials on localhost:5432.
|
||||
- S3/Storage and SMTP vars can be left empty — the app falls back to local filesystem and console-logged emails.
|
||||
|
||||
### Common commands
|
||||
|
||||
See `scripts` in `package.json`. Key ones:
|
||||
|
||||
| Task | Command |
|
||||
|---|---|
|
||||
| Dev server | `pnpm dev` (port 3000) |
|
||||
| Lint (Biome) | `pnpm lint` |
|
||||
| Typecheck | `pnpm typecheck` |
|
||||
| DB migrations | `pnpm db:generate` / `pnpm db:migrate` (auto-runs on dev start) |
|
||||
|
||||
### Gotchas
|
||||
|
||||
- The Docker daemon needs `fuse-overlayfs` storage driver and `iptables-legacy` in the cloud VM (nested container environment).
|
||||
- `pnpm.onlyBuiltDependencies` in `package.json` controls which packages are allowed to run install scripts — no interactive `pnpm approve-builds` needed.
|
||||
- Email verification is optional in dev — after signup, click "Continue" to skip.
|
||||
- Vite 8 is beta (`^8.0.0-beta.15`); Nitro uses a nightly build. Occasional upstream issues may occur.
|
||||
@@ -35,6 +35,18 @@ RUN pnpm run build
|
||||
# ---------- Runtime Layer ----------
|
||||
FROM node:24-slim AS runtime
|
||||
|
||||
LABEL maintainer="amruthpillai"
|
||||
LABEL org.opencontainers.image.licenses="MIT"
|
||||
LABEL org.opencontainers.image.title="Reactive Resume"
|
||||
LABEL org.opencontainers.image.description="A free and open-source resume builder."
|
||||
LABEL org.opencontainers.image.vendor="Amruth Pillai"
|
||||
LABEL org.opencontainers.image.url="https://rxresu.me"
|
||||
LABEL org.opencontainers.image.documentation="https://docs.rxresu.me"
|
||||
LABEL org.opencontainers.image.source="https://github.com/amruthpillai/reactive-resume"
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
@@ -45,4 +57,7 @@ COPY --from=dependencies /tmp/prod/node_modules ./node_modules
|
||||
|
||||
EXPOSE 3000/tcp
|
||||
|
||||
ENTRYPOINT ["node", "-r", "reflect-metadata", ".output/server/index.mjs"]
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD curl -f http://localhost:3000/api/health || exit 1
|
||||
|
||||
ENTRYPOINT ["node", ".output/server/index.mjs"]
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
# Feature Request: AI Chat Builder for Resume Editing
|
||||
|
||||
I don't know much about this, so I'm looking for volunteers to help with the implementation of this feature I'd like to see on Reactive Resume, and I hope the community finds interest in this feature too.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
For this feature to work, the following conditions must be met:
|
||||
|
||||
- User is logged into Reactive Resume
|
||||
- User has set up AI integration on their account
|
||||
- This means that an AI provider, model and API key should be available for usage on the browser through `useAIStore`
|
||||
|
||||
## User Story
|
||||
|
||||
1. I have just created a new resume, or I am currently editing a resume I made earlier and I'm on the builder screen.
|
||||
2. I know what I want, I need to make a few edits here and there, rewrite my summary so it reads better, but I'm lazy.
|
||||
3. I see a button (outline) on the header, right next to the name on the resume and the menu icon that reads **"Build with AI"** and has a sparkles ✨ icon on its left.
|
||||
- This button is only enabled if I have AI integration enabled. If I don't, this button shouldn't be visible at all so as to not hinder the user experience for non-AI enabled users.
|
||||
4. Clicking on the button displays a chat overlay that can be toggled open/close which covers the right portion of the screen (about 380px width).
|
||||
5. The chat window should animate open, sliding from the bottom. Below the window, there should be a floating action button which controls the visibility of this window. If the button hasn't been interacted with in the last 1 minute, hide the button until the user clicks on the "Build with AI" button again.
|
||||
6. The AI chat should display an initial greeting message. Users can type in what they want to change specifically in the resume, some examples include:
|
||||
- "Rewrite the professional summary"
|
||||
- "Translate all of the section headings to German"
|
||||
- "Make my work experience descriptions more impactful"
|
||||
- "Add keywords related to software engineering"
|
||||
7. The request should be sent using `await client.ai.chat({ input: { aiStoreData, resumeId } })`. Ideally, the oRPC chat route should explicitly accept the `resumeId` in its input and have it available in the context, ensuring that any AI-driven changes or updates are accurately mapped to the correct resume.
|
||||
8. This should display the changes to be made to the resume and apply them. Maybe using tools? I'm not too sure if that's what tools are used for.
|
||||
|
||||
## What Already Exists
|
||||
|
||||
The codebase already has the foundational pieces in place:
|
||||
|
||||
### AI Store (`src/integrations/ai/store.ts`)
|
||||
|
||||
Manages the AI configuration stored in localStorage:
|
||||
|
||||
```typescript
|
||||
type AIStoreState = {
|
||||
enabled: boolean;
|
||||
provider: AIProvider; // "openai" | "gemini" | "anthropic" | "ollama" | ...;
|
||||
model: string;
|
||||
apiKey: string;
|
||||
baseURL: string;
|
||||
testStatus: TestStatus;
|
||||
};
|
||||
```
|
||||
|
||||
You can check if AI is enabled via:
|
||||
|
||||
```typescript
|
||||
const enabled = useAIStore((state) => state.enabled);
|
||||
```
|
||||
|
||||
### oRPC AI Router (`src/integrations/orpc/router/ai.ts`)
|
||||
|
||||
Existing AI endpoints that demonstrate streaming patterns:
|
||||
|
||||
```typescript
|
||||
// Example: testConnection streams a response
|
||||
testConnection: protectedProcedure
|
||||
.input(z.object({ provider, model, apiKey, baseURL }))
|
||||
.handler(async function* ({ input }) {
|
||||
const stream = streamText({
|
||||
model: getModel(input),
|
||||
messages: [{ role: "user", content: 'Respond with "1"' }],
|
||||
});
|
||||
yield* stream.textStream;
|
||||
}),
|
||||
```
|
||||
|
||||
The router also uses `generateText` with structured output for parsing documents - this pattern could be useful for applying changes to resume data.
|
||||
|
||||
### Resume Store (`src/components/resume/store/resume.ts`)
|
||||
|
||||
Provides `updateResumeData()` for modifying the resume:
|
||||
|
||||
```typescript
|
||||
updateResumeData: (fn) => {
|
||||
set((state) => {
|
||||
if (!state.resume) return state;
|
||||
if (state.resume.isLocked) {
|
||||
// show error toast
|
||||
return state;
|
||||
}
|
||||
fn(state.resume.data as WritableDraft<ResumeData>);
|
||||
syncResume(current(state.resume));
|
||||
});
|
||||
},
|
||||
```
|
||||
|
||||
### Builder Header (`src/routes/builder/$resumeId/-components/header.tsx`)
|
||||
|
||||
This is where the "Build with AI" button should be placed, next to the resume name and dropdown menu.
|
||||
|
||||
## Technical Considerations
|
||||
|
||||
Here are some implementation hints for anyone interested in picking this up:
|
||||
|
||||
### 1. Button Placement
|
||||
|
||||
Add the button in the `BuilderHeader` component, conditionally rendered based on `useAIStore().enabled`:
|
||||
|
||||
```typescript
|
||||
const enabled = useAIStore((state) => state.enabled);
|
||||
|
||||
// In the JSX, only show if enabled
|
||||
{enabled && (
|
||||
<Button variant="outline" onClick={openChatPanel}>
|
||||
<SparkleIcon />
|
||||
Build with AI
|
||||
</Button>
|
||||
)}
|
||||
```
|
||||
|
||||
### 2. Chat Panel Component
|
||||
|
||||
- Create a new component (e.g., `AIChatPanel`) that renders as an overlay
|
||||
- Width: ~380px, positioned on the right side of the screen
|
||||
- Animate in from the bottom using Framer Motion (already in the project as `motion/react`)
|
||||
- Include a floating action button (FAB) below the panel for quick toggle
|
||||
- Auto-hide the FAB after 1 minute of inactivity
|
||||
|
||||
### 3. Chat State Management
|
||||
|
||||
Consider creating a new Zustand store for chat state:
|
||||
|
||||
```typescript
|
||||
type AIChatStore = {
|
||||
isOpen: boolean;
|
||||
messages: ChatMessage[];
|
||||
lastInteraction: number;
|
||||
// actions...
|
||||
};
|
||||
```
|
||||
|
||||
### 4. oRPC Route for Chat
|
||||
|
||||
Create a new route in `src/integrations/orpc/router/ai.ts`:
|
||||
|
||||
```typescript
|
||||
chat: protectedProcedure
|
||||
.input(z.object({
|
||||
...aiCredentialsSchema.shape,
|
||||
resumeId: z.string(),
|
||||
message: z.string(),
|
||||
}))
|
||||
.handler(async function* ({ input, context }) {
|
||||
// Get resume data for context
|
||||
// Stream AI response
|
||||
// Optionally use tools for structured changes
|
||||
}),
|
||||
```
|
||||
|
||||
### 5. Applying Changes
|
||||
|
||||
The tricky part is how to apply AI-suggested changes to the resume. Some options:
|
||||
|
||||
- **AI SDK Tools**: Use the Vercel AI SDK's tool calling feature to let the AI invoke specific functions like `updateSummary`, `translateHeadings`, etc.
|
||||
- **Structured Output**: Have the AI return a JSON patch or specific update instructions that can be applied via `updateResumeData()`
|
||||
- **Diff Display**: Show proposed changes before applying them, letting users accept/reject
|
||||
|
||||
### 6. Resume Data Schema
|
||||
|
||||
The resume data schema is defined in `src/schema/resume/data.ts` and includes sections like:
|
||||
- `basics` (name, headline, summary, contact info)
|
||||
- `experience`, `education`, `skills`, `projects`, etc.
|
||||
- `metadata` (template, typography, colors)
|
||||
- `customSections`
|
||||
|
||||
## Design Principles
|
||||
|
||||
The premise is simple and nothing that hasn't been done before, but I'd like to see it done **subtly** as this is not an AI-first app, it should just be AI-enabled.
|
||||
|
||||
- The feature should feel like an optional enhancement, not a core requirement
|
||||
- Non-AI users should not feel like they're missing out or see disabled features
|
||||
- The UI should be clean and non-intrusive
|
||||
- Performance should not be impacted when AI features are not in use
|
||||
|
||||
## Getting Started
|
||||
|
||||
If you're interested in contributing:
|
||||
|
||||
1. Fork the repository
|
||||
2. Set up the development environment (see README)
|
||||
3. Configure AI settings in Dashboard > Settings > AI
|
||||
4. Start exploring the code references mentioned above
|
||||
|
||||
## Questions?
|
||||
|
||||
Feel free to ask questions in the comments below. I'm happy to provide more context or clarify any requirements.
|
||||
|
||||
---
|
||||
|
||||
**Labels**: `enhancement`, `help wanted`, `good first issue`, `ai`
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Amruth Pillai
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,17 +1,3 @@
|
||||
<!--
|
||||
<div align="center" markdown="1">
|
||||
<a href="https://go.warp.dev/Reactive-Resume">
|
||||
<img alt="Warp Sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-03.png?raw=true" />
|
||||
</a>
|
||||
|
||||
### [Warp, built for coding with multiple AI agents.](https://go.warp.dev/Reactive-Resume)
|
||||
|
||||
[Available for MacOS, Linux, & Windows](https://go.warp.dev/Reactive-Resume)
|
||||
</div>
|
||||
|
||||
---
|
||||
-->
|
||||
|
||||
<div align="center">
|
||||
<a href="https://rxresu.me">
|
||||
<img src="public/opengraph/banner.jpg" alt="Reactive Resume" />
|
||||
@@ -28,15 +14,15 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<img src="https://img.shields.io/github/stars/AmruthPillai/Reactive-Resume?style=flat-square" alt="Stars" />
|
||||
<img src="https://img.shields.io/github/license/AmruthPillai/Reactive-Resume?style=flat-square" alt="License" />
|
||||
<img src="https://img.shields.io/github/package-json/v/amruthpillai/reactive-resume?style=flat-square" alt="Reactive Resume version">
|
||||
<img src="https://img.shields.io/github/stars/amruthpillai/Reactive-Resume?style=flat-square" alt="GitHub Stars">
|
||||
<img src="https://img.shields.io/github/license/amruthpillai/Reactive-Resume?style=flat-square" alt="License" />
|
||||
<img src="https://img.shields.io/docker/pulls/amruthpillai/reactive-resume?style=flat-square" alt="Docker Pulls" />
|
||||
<a href="https://discord.gg/hzwkZbyvUW"><img src="https://img.shields.io/discord/1173518977851473940?label=Discord&style=flat-square&logo=discord" alt="Discord" /></a>
|
||||
<a href="https://discord.gg/aSyA5ZSxpb"><img src="https://img.shields.io/discord/1173518977851473940?style=flat-square&label=discord" alt="Discord" /></a>
|
||||
<a href="https://crowdin.com/project/reactive-resume"><img src="https://badges.crowdin.net/reactive-resume/localized.svg?style=flat-square" alt="Crowdin" /></a>
|
||||
<a href="https://opencollective.com/reactive-resume"><img src="https://img.shields.io/opencollective/all/reactive-resume?style=flat-square" alt="Open Collective" /></a>
|
||||
<a href="https://github.com/sponsors/AmruthPillai"><img src="https://img.shields.io/github/sponsors/AmruthPillai?style=flat-square&label=sponsors" alt="Sponsors" /></a>
|
||||
<a href="https://opencollective.com/reactive-resume"><img src="https://img.shields.io/opencollective/backers/reactive-resume?style=flat-square&label=donations" alt="Donations" /></a>
|
||||
</p>
|
||||
|
||||
<a href="https://www.producthunt.com/products/reactive-resume?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-reactive-resume-v5-2" target="_blank" rel="noopener noreferrer"><img alt="Reactive Resume v5 - A free and open-source resume builder. | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1065182&theme=light&t=1768850745585"></a>
|
||||
</div>
|
||||
|
||||
---
|
||||
@@ -149,8 +135,8 @@ The quickest way to run Reactive Resume locally:
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/AmruthPillai/Reactive-Resume.git
|
||||
cd Reactive-Resume
|
||||
git clone https://github.com/amruthpillai/reactive-resume.git
|
||||
cd reactive-resume
|
||||
|
||||
# Start all services
|
||||
docker compose up -d
|
||||
@@ -159,6 +145,8 @@ docker compose up -d
|
||||
open http://localhost:3000
|
||||
```
|
||||
|
||||
[](https://app.ona.com/#https://github.com/amruthpillai/reactive-resume)
|
||||
|
||||
For detailed setup instructions, environment configuration, and self-hosting guides, see the [documentation](https://docs.rxresu.me).
|
||||
|
||||
## Tech Stack
|
||||
@@ -182,7 +170,7 @@ Comprehensive guides are available at [docs.rxresu.me](https://docs.rxresu.me):
|
||||
| Guide | Description |
|
||||
| --------------------------------------------------------------------------- | --------------------------------- |
|
||||
| [Getting Started](https://docs.rxresu.me/getting-started) | First-time setup and basic usage |
|
||||
| [Self-Hosting](https://docs.rxresu.me/guides/self-hosting-with-docker) | Deploy on your own server |
|
||||
| [Self-Hosting](https://docs.rxresu.me/self-hosting/docker) | Deploy on your own server |
|
||||
| [Development Setup](https://docs.rxresu.me/contributing/development) | Local development environment |
|
||||
| [Project Architecture](https://docs.rxresu.me/contributing/architecture) | Codebase structure and patterns |
|
||||
| [Exporting Your Resume](https://docs.rxresu.me/guides/exporting-your-resume)| PDF and JSON export options |
|
||||
@@ -192,7 +180,7 @@ Comprehensive guides are available at [docs.rxresu.me](https://docs.rxresu.me):
|
||||
Reactive Resume can be self-hosted using Docker. The stack includes:
|
||||
|
||||
- **PostgreSQL** — Database for storing user data and resumes
|
||||
- **Gotenberg** — Headless Chrome service for PDF generation
|
||||
- **Printer** — Headless Chromium service for PDF and screenshot generation
|
||||
- **SeaweedFS** (optional) — S3-compatible storage for file uploads
|
||||
|
||||
Pull the latest image from Docker Hub or GitHub Container Registry:
|
||||
@@ -205,7 +193,7 @@ docker pull amruthpillai/reactive-resume:latest
|
||||
docker pull ghcr.io/amruthpillai/reactive-resume:latest
|
||||
```
|
||||
|
||||
See the [self-hosting guide](https://docs.rxresu.me/guides/self-hosting-with-docker) for complete instructions.
|
||||
See the [self-hosting guide](https://docs.rxresu.me/self-hosting/docker) for complete instructions.
|
||||
|
||||
## Support
|
||||
|
||||
@@ -227,6 +215,16 @@ Other ways to support:
|
||||
- Improve documentation
|
||||
- Help with translations
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/#amruthpillai/reactive-resume&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=amruthpillai/reactive-resume&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=amruthpillai/reactive-resume&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=amruthpillai/reactive-resume&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions make open-source thrive. Whether fixing a typo or adding a feature, all contributions are welcome.
|
||||
|
||||
@@ -8,7 +8,13 @@
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": ["**", "!**/webfontlist.json", "!**/docs/spec.json", "!**/src/routeTree.gen.ts"]
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/docs/spec.json",
|
||||
"!**/webfontlist.json",
|
||||
"!**/schema/schema.json",
|
||||
"!**/src/routeTree.gen.ts"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"lineWidth": 120,
|
||||
@@ -18,8 +24,7 @@
|
||||
"actions": {
|
||||
"recommended": true,
|
||||
"source": {
|
||||
"organizeImports": "on",
|
||||
"useSortedProperties": "on"
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -32,6 +37,7 @@
|
||||
},
|
||||
"correctness": {
|
||||
"noUnknownTypeSelector": "off",
|
||||
"useExhaustiveDependencies": "off",
|
||||
"noUnusedImports": {
|
||||
"fix": "safe",
|
||||
"level": "error",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/styles.css",
|
||||
"baseColor": "neutral",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
@@ -15,12 +15,9 @@
|
||||
"menuAccent": "subtle",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/utils",
|
||||
"utils": "@/utils/style",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/utils",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {
|
||||
"@animate-ui": "https://animate-ui.com/r/{name}.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
name: reactive_resume
|
||||
|
||||
services:
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080"]
|
||||
start_period: 10s
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
postgres:
|
||||
image: postgres:18
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
- POSTGRES_DB=postgres
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql
|
||||
ports:
|
||||
@@ -19,25 +34,30 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:8
|
||||
browserless:
|
||||
image: ghcr.io/browserless/chromium:latest
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
- WEBHOOK_DISABLE=true
|
||||
- CHROMIUM_AUTO_START=true
|
||||
- LIBREOFFICE_DISABLE_ROUTES=true
|
||||
- PROMETHEUS_DISABLE_COLLECT=true
|
||||
- CHROMIUM_HOST_RESOLVER_RULES=MAP localhost host.docker.internal
|
||||
ports:
|
||||
- "4000:3000"
|
||||
environment:
|
||||
- QUEUED=10
|
||||
- HEALTH=true
|
||||
- CONCURRENT=5
|
||||
- TOKEN=1234567890
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
start_period: 10s
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
test:
|
||||
["CMD", "curl", "-f", "http://localhost:3000/pressure?token=1234567890"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# As an alternative to browserless, you can also use a lightweight image like chromedp/headless-shell:latest
|
||||
# See https://docs.rxresu.me/self-hosting/docker#alternative-printer-options for more information.
|
||||
chrome:
|
||||
image: chromedp/headless-shell:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9222:9222"
|
||||
|
||||
seaweedfs:
|
||||
image: chrislusf/seaweedfs:latest
|
||||
@@ -57,7 +77,7 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
seaweedfs-create-bucket:
|
||||
seaweedfs_create_bucket:
|
||||
image: quay.io/minio/mc:latest
|
||||
restart: on-failure
|
||||
entrypoint: >
|
||||
@@ -88,6 +108,6 @@ services:
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
mailpit_data:
|
||||
postgres_data:
|
||||
seaweedfs_data:
|
||||
mailpit_data:
|
||||
|
||||
@@ -2,12 +2,12 @@ name: reactive_resume
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
- POSTGRES_DB=postgres
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
@@ -17,22 +17,28 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:8
|
||||
browserless:
|
||||
image: ghcr.io/browserless/chromium:latest
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
- WEBHOOK_DISABLE=true
|
||||
- CHROMIUM_AUTO_START=true
|
||||
- LIBREOFFICE_DISABLE_ROUTES=true
|
||||
- PROMETHEUS_DISABLE_COLLECT=true
|
||||
- QUEUED=10
|
||||
- HEALTH=true
|
||||
- CONCURRENT=5
|
||||
- TOKEN=1234567890
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
start_period: 10s
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
test:
|
||||
["CMD", "curl", "-f", "http://localhost:3000/pressure?token=1234567890"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# As an alternative to browserless, you can also use a lightweight image like chromedp/headless-shell:latest
|
||||
# See https://docs.rxresu.me/self-hosting/docker#alternative-printer-options for more information.
|
||||
# chrome:
|
||||
# image: chromedp/headless-shell:latest
|
||||
# restart: unless-stopped
|
||||
# ports:
|
||||
# - "9222:9222"
|
||||
|
||||
seaweedfs:
|
||||
image: chrislusf/seaweedfs:latest
|
||||
@@ -50,7 +56,7 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
seaweedfs-create-bucket:
|
||||
seaweedfs_create_bucket:
|
||||
image: quay.io/minio/mc:latest
|
||||
restart: on-failure
|
||||
entrypoint: >
|
||||
@@ -64,7 +70,7 @@ services:
|
||||
seaweedfs:
|
||||
condition: service_healthy
|
||||
|
||||
app:
|
||||
reactive_resume:
|
||||
image: amruthpillai/reactive-resume:latest
|
||||
# image: ghcr.io/amruthpillai/reactive-resume:latest
|
||||
environment:
|
||||
@@ -72,9 +78,10 @@ services:
|
||||
- TZ=Etc/UTC
|
||||
- NODE_ENV=production
|
||||
- APP_URL=http://localhost:3000
|
||||
- PRINTER_APP_URL=http://app:3000
|
||||
- PRINTER_APP_URL=http://reactive_resume:3000
|
||||
# Printer
|
||||
- GOTENBERG_ENDPOINT=http://gotenberg:3000
|
||||
- PRINTER_ENDPOINT=ws://browserless:3000?token=1234567890
|
||||
# - PRINTER_ENDPOINT=http://chrome:9222 # Or, if you're using chromedp/headless-shell
|
||||
# Database
|
||||
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/postgres
|
||||
# Authentication
|
||||
@@ -92,18 +99,12 @@ services:
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
gotenberg:
|
||||
browserless:
|
||||
condition: service_healthy
|
||||
seaweedfs-create-bucket:
|
||||
seaweedfs_create_bucket:
|
||||
condition: service_completed_successfully
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))",
|
||||
]
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||
start_period: 10s
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
project_id_env: CROWDIN_PROJECT_ID
|
||||
api_token_env: CROWDIN_API_TOKEN
|
||||
|
||||
preserve_hierarchy: true
|
||||
commit_message: "[ci skip]"
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
FROM postgres:latest
|
||||
|
||||
COPY ./postgresql.conf /etc/postgresql/postgresql.conf
|
||||
|
||||
COPY ./init.sql /docker-entrypoint-initdb.d/
|
||||
|
||||
RUN chown postgres:postgres /etc/postgresql/postgresql.conf && \
|
||||
chmod 644 /etc/postgresql/postgresql.conf
|
||||
|
||||
EXPOSE 5432
|
||||
|
||||
CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]
|
||||
@@ -1,46 +0,0 @@
|
||||
-- Enable pg_stat_statements extension
|
||||
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
||||
|
||||
-- Enable other useful extensions
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- UUID generation
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- Cryptographic functions
|
||||
CREATE EXTENSION IF NOT EXISTS "btree_gin"; -- GIN indexes for btree types
|
||||
CREATE EXTENSION IF NOT EXISTS "btree_gist"; -- GIST indexes for btree types
|
||||
|
||||
-- Create a function to reset pg_stat_statements (useful for debugging)
|
||||
CREATE OR REPLACE FUNCTION reset_query_stats()
|
||||
RETURNS void AS $$
|
||||
BEGIN
|
||||
PERFORM pg_stat_statements_reset();
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create a view for easier query analysis
|
||||
CREATE OR REPLACE VIEW slow_queries AS
|
||||
SELECT
|
||||
query,
|
||||
calls,
|
||||
ROUND(total_exec_time::numeric, 2) AS total_time_ms,
|
||||
ROUND(mean_exec_time::numeric, 2) AS mean_time_ms,
|
||||
ROUND(max_exec_time::numeric, 2) AS max_time_ms,
|
||||
ROUND((100 * total_exec_time / SUM(total_exec_time) OVER ())::numeric, 2) AS percentage,
|
||||
rows
|
||||
FROM pg_stat_statements
|
||||
ORDER BY total_exec_time DESC;
|
||||
|
||||
-- Create a view for most frequent queries
|
||||
CREATE OR REPLACE VIEW frequent_queries AS
|
||||
SELECT
|
||||
query,
|
||||
calls,
|
||||
ROUND(mean_exec_time::numeric, 2) AS mean_time_ms,
|
||||
ROUND(total_exec_time::numeric, 2) AS total_time_ms,
|
||||
rows
|
||||
FROM pg_stat_statements
|
||||
ORDER BY calls DESC;
|
||||
|
||||
-- Notification for successful initialization
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'Database initialized with pg_stat_statements and helper views';
|
||||
END $$;
|
||||
@@ -1,122 +0,0 @@
|
||||
# -----------------------------
|
||||
# PostgreSQL 18 configuration file
|
||||
# Optimized for: 8GB RAM, 6 vCPU, KVM Virtual Machine
|
||||
# -----------------------------
|
||||
|
||||
# CONNECTIONS AND AUTHENTICATION
|
||||
listen_addresses = '*'
|
||||
max_connections = 100
|
||||
superuser_reserved_connections = 3
|
||||
|
||||
# MEMORY SETTINGS
|
||||
shared_buffers = 2GB
|
||||
huge_pages = try
|
||||
effective_cache_size = 6GB
|
||||
maintenance_work_mem = 512MB
|
||||
work_mem = 20MB
|
||||
|
||||
# QUERY TUNING
|
||||
random_page_cost = 1.1
|
||||
effective_io_concurrency = 200
|
||||
default_statistics_target = 100
|
||||
|
||||
# WRITE AHEAD LOG (WAL)
|
||||
wal_level = replica
|
||||
wal_buffers = 16MB
|
||||
min_wal_size = 1GB
|
||||
max_wal_size = 4GB
|
||||
wal_compression = on
|
||||
checkpoint_completion_target = 0.9
|
||||
checkpoint_timeout = 15min
|
||||
|
||||
# BACKGROUND WRITER
|
||||
bgwriter_delay = 200ms
|
||||
bgwriter_lru_maxpages = 100
|
||||
bgwriter_lru_multiplier = 2.0
|
||||
|
||||
# AUTOVACUUM
|
||||
autovacuum = on
|
||||
autovacuum_max_workers = 3
|
||||
autovacuum_naptime = 1min
|
||||
autovacuum_vacuum_threshold = 50
|
||||
autovacuum_vacuum_scale_factor = 0.1
|
||||
autovacuum_analyze_threshold = 50
|
||||
autovacuum_analyze_scale_factor = 0.05
|
||||
autovacuum_vacuum_cost_delay = 2ms
|
||||
autovacuum_vacuum_cost_limit = 200
|
||||
|
||||
# MONITORING AND STATISTICS
|
||||
track_activities = on
|
||||
track_counts = on
|
||||
track_io_timing = on
|
||||
track_functions = all
|
||||
track_wal_io_timing = on
|
||||
compute_query_id = on
|
||||
|
||||
# pg_stat_statements configuration
|
||||
pg_stat_statements.track = all
|
||||
pg_stat_statements.max = 10000
|
||||
pg_stat_statements.track_utility = on
|
||||
pg_stat_statements.save = on
|
||||
|
||||
# LOGGING
|
||||
log_destination = 'stderr'
|
||||
logging_collector = on
|
||||
log_directory = 'log'
|
||||
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
|
||||
log_file_mode = 0600
|
||||
log_rotation_age = 1d
|
||||
log_rotation_size = 100MB
|
||||
log_truncate_on_rotation = on
|
||||
|
||||
# What to log
|
||||
log_min_duration_statement = 1000
|
||||
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
|
||||
log_checkpoints = on
|
||||
log_connections = on
|
||||
log_disconnections = on
|
||||
log_duration = off
|
||||
log_lock_waits = on
|
||||
log_statement = 'none'
|
||||
log_temp_files = 0
|
||||
log_timezone = 'UTC'
|
||||
|
||||
# LOCALE AND FORMATTING
|
||||
datestyle = 'iso, mdy'
|
||||
timezone = 'UTC'
|
||||
lc_messages = 'en_US.utf8'
|
||||
lc_monetary = 'en_US.utf8'
|
||||
lc_numeric = 'en_US.utf8'
|
||||
lc_time = 'en_US.utf8'
|
||||
default_text_search_config = 'pg_catalog.english'
|
||||
|
||||
# LOCK MANAGEMENT
|
||||
deadlock_timeout = 1s
|
||||
max_locks_per_transaction = 64
|
||||
max_pred_locks_per_transaction = 64
|
||||
|
||||
# CLIENT CONNECTION DEFAULTS
|
||||
search_path = '"$user", public'
|
||||
idle_in_transaction_session_timeout = 600000
|
||||
statement_timeout = 0
|
||||
|
||||
# PARALLEL QUERY EXECUTION
|
||||
max_worker_processes = 6
|
||||
max_parallel_workers_per_gather = 3
|
||||
max_parallel_workers = 6
|
||||
max_parallel_maintenance_workers = 3
|
||||
parallel_leader_participation = on
|
||||
|
||||
# INCREMENTAL BACKUP
|
||||
summarize_wal = on
|
||||
|
||||
# VACUUM OPTIMIZATION
|
||||
vacuum_buffer_usage_limit = 256kB
|
||||
|
||||
# VECTOR EXTENSION SETTINGS
|
||||
maintenance_work_mem = 512MB
|
||||
max_parallel_maintenance_workers = 3
|
||||
work_mem = 32MB
|
||||
|
||||
# EXTENSIONS
|
||||
shared_preload_libraries = 'pg_stat_statements'
|
||||
@@ -1,206 +0,0 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
command: postgres -c config_file=/etc/postgresql.conf
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
volumes:
|
||||
- reactive_resume_postgres_data:/var/lib/postgresql
|
||||
environment:
|
||||
- POSTGRES_DB=$POSTGRES_DB
|
||||
- POSTGRES_USER=$POSTGRES_USER
|
||||
- POSTGRES_PASSWORD=$POSTGRES_PASSWORD
|
||||
configs:
|
||||
- source: reactive_resume_postgres_config
|
||||
target: /etc/postgresql.conf
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
window: 120s
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:8
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
environment:
|
||||
- WEBHOOK_DISABLE=true
|
||||
- CHROMIUM_AUTO_START=true
|
||||
- API_ENABLE_BASIC_AUTH=true
|
||||
- PROMETHEUS_DISABLE_COLLECT=true
|
||||
- GOTENBERG_API_BASIC_AUTH_USERNAME=$GOTENBERG_USERNAME
|
||||
- GOTENBERG_API_BASIC_AUTH_PASSWORD=$GOTENBERG_PASSWORD
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 2
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
window: 120s
|
||||
|
||||
seaweedfs:
|
||||
image: chrislusf/seaweedfs:latest
|
||||
command: server -s3 -filer -dir=/data -ip=0.0.0.0
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
volumes:
|
||||
- reactive_resume_seaweedfs_data:/data
|
||||
environment:
|
||||
- AWS_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
|
||||
- AWS_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:8888"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
window: 120s
|
||||
|
||||
seaweedfs_create_bucket:
|
||||
image: quay.io/minio/mc:latest
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
until mc alias set seaweedfs http://seaweedfs:8333 $S3_ACCESS_KEY_ID $S3_SECRET_ACCESS_KEY; do
|
||||
echo 'Waiting for SeaweedFS...';
|
||||
sleep 2;
|
||||
done;
|
||||
mc mb seaweedfs/$S3_BUCKET --ignore-existing;
|
||||
"
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 10s
|
||||
max_attempts: 5
|
||||
window: 120s
|
||||
|
||||
app:
|
||||
image: ghcr.io/amruthpillai/reactive-resume-test:latest
|
||||
networks:
|
||||
- traefik_network
|
||||
- reactive_resume_network
|
||||
volumes:
|
||||
- reactive_resume_app_data:/app/data
|
||||
environment:
|
||||
- APP_URL=$APP_URL
|
||||
- GOTENBERG_ENDPOINT=$GOTENBERG_ENDPOINT
|
||||
- GOTENBERG_USERNAME=$GOTENBERG_USERNAME
|
||||
- GOTENBERG_PASSWORD=$GOTENBERG_PASSWORD
|
||||
- DATABASE_URL=$DATABASE_URL
|
||||
- AUTH_SECRET=$AUTH_SECRET
|
||||
- GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID
|
||||
- GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET
|
||||
- GITHUB_CLIENT_ID=$GITHUB_CLIENT_ID
|
||||
- GITHUB_CLIENT_SECRET=$GITHUB_CLIENT_SECRET
|
||||
- SMTP_HOST=$SMTP_HOST
|
||||
- SMTP_PORT=$SMTP_PORT
|
||||
- SMTP_USER=$SMTP_USER
|
||||
- SMTP_PASS=$SMTP_PASS
|
||||
- SMTP_FROM=$SMTP_FROM
|
||||
- SMTP_SECURE=$SMTP_SECURE
|
||||
- S3_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
|
||||
- S3_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
|
||||
- S3_REGION=$S3_REGION
|
||||
- S3_ENDPOINT=$S3_ENDPOINT
|
||||
- S3_BUCKET=$S3_BUCKET
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 2
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
window: 120s
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
failure_action: rollback
|
||||
order: start-first
|
||||
rollback_config:
|
||||
parallelism: 1
|
||||
delay: 10s
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.app.rule=Host(`rxresu.me`)"
|
||||
- "traefik.http.routers.app.entrypoints=websecure"
|
||||
- "traefik.http.routers.app.tls=true"
|
||||
- "traefik.http.services.app.loadbalancer.server.port=3000"
|
||||
|
||||
configs:
|
||||
reactive_resume_postgres_config:
|
||||
name: reactive_resume_postgres_config
|
||||
external: true
|
||||
|
||||
networks:
|
||||
traefik_network:
|
||||
external: true
|
||||
reactive_resume_network:
|
||||
name: reactive_resume_network
|
||||
driver: overlay
|
||||
attachable: true
|
||||
|
||||
volumes:
|
||||
reactive_resume_postgres_data:
|
||||
name: reactive_resume_postgres_data
|
||||
reactive_resume_seaweedfs_data:
|
||||
name: reactive_resume_seaweedfs_data
|
||||
reactive_resume_app_data:
|
||||
name: reactive_resume_app_data
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
title: "Changelog"
|
||||
description: "List of all notable changes and updates to Reactive Resume"
|
||||
rss: true
|
||||
---
|
||||
|
||||
<Update label="v5.0.10" description="24th February 2026">
|
||||
- **Fixes**
|
||||
- Show section titles for summary-type custom sections in the resume builder. [#2744](https://github.com/amruthpillai/reactive-resume/pull/2744)
|
||||
- Prevent browser password managers and Edge autofill/save prompts from appearing on AI settings API key fields. [#2719](https://github.com/amruthpillai/reactive-resume/pull/2719)
|
||||
- Replace deprecated Tailwind CSS classes: use `inset-s-*`/`inset-e-*` instead of `start-*`/`end-*`.
|
||||
- Fix PDF downloader to work correctly in offline mode. [#2743](https://github.com/amruthpillai/reactive-resume/pull/2743)
|
||||
- Make bold formatting visible for `<strong>` in the resume rich text editor (uses plain `font-weight: bold` fallback if the CSS variable is unset). Fixes [#2730](https://github.com/amruthpillai/reactive-resume/issues/2730)
|
||||
- Prevent credentials sign-in from dropping `session_token` Set-Cookie, improves login reliability. [#2718](https://github.com/amruthpillai/reactive-resume/pull/2718)
|
||||
- Remove redundant `resume-` prefix from download filename; add spacing between pages in shared view. [#2709](https://github.com/amruthpillai/reactive-resume/pull/2709)
|
||||
- Normalize autocomplete tokens for login and register forms. [#2714](https://github.com/amruthpillai/reactive-resume/pull/2714)
|
||||
- Fix improper chips reordering and update dependency/translations. [#2711](https://github.com/amruthpillai/reactive-resume/issues/2711)
|
||||
- Fix issue with clipping of heading in Lapras resume template.
|
||||
- Remove error-causing plugins in oRPC integration.
|
||||
- Remove duplicate database indexes; add index for `created_at` on user and resume tables.
|
||||
|
||||
- **Features & Improvements**
|
||||
- Add new feature flag: `FLAG_DISABLE_IMAGE_PROCESSING` (allows disabling of image processing site-wide).
|
||||
- Update Discord invite link in the app and documentation.
|
||||
- Add comprehensive codebase and architecture documentation in `CLAUDE.md`.
|
||||
- Sync latest translations from Crowdin (updated and added multiple languages).
|
||||
|
||||
- **Other**
|
||||
- Refactor: rename auth utility from `originWith` to `withHostname`, preserve localhost/127.0.0.1 sibling trust.
|
||||
- General dependency updates and code style improvements.
|
||||
- Documentation and README updates.
|
||||
</Update>
|
||||
|
||||
|
||||
<Update label="v5.0.9" description="9th February 2026">
|
||||
- Add Computer Modern web fonts to the font selector, allowing the user to choose from a variety of "Computer Modern" (LaTeX) fonts.
|
||||
- This lets you create a resume that looks just like a LaTeX document, with the same fonts and styles.
|
||||
- Update dependencies to the latest versions.
|
||||
</Update>
|
||||
|
||||
<Update label="v5.0.8" description="9th February 2026">
|
||||
- Remove Passkey support from the authentication system, as it was causing issues with the authentication provider.
|
||||
- Update dependencies to the latest versions.
|
||||
</Update>
|
||||
|
||||
<Update label="v5.0.7" description="9th February 2026">
|
||||
- Introduce a new **MCP (Model Context Protocol) server** that lets you manage and edit resumes from any MCP-compatible AI tool — Claude Desktop, Cursor, Codex, and more. Supports listing, reading, creating, deleting, locking/unlocking, and patching resumes via natural language. [(guide)](/guides/using-the-mcp-server)
|
||||
- Add an **AI Chat** panel to the resume builder, allowing you to modify your resume through conversational AI directly within the editor. The chat uses tool-calling to apply JSON Patch operations to your resume in real-time, with visual feedback for each change.
|
||||
- Add a system prompt and `patch_resume` tool for the AI chat, enabling structured, minimal-diff resume edits following RFC 6902 JSON Patch operations.
|
||||
- Chat history is now persisted per resume in localStorage, so conversations are preserved across sessions.
|
||||
- Fix rendering issues in the Lapras and Onyx resume templates.
|
||||
- Improvements to the Combobox and ScrollArea UI components.
|
||||
- Fix an issue with skills item rendering in the shared resume components.
|
||||
- Update authentication configuration and auth route handling.
|
||||
- Update the JSON Schema to reflect the latest resume data model.
|
||||
- Update dependencies to the latest versions.
|
||||
</Update>
|
||||
|
||||
<Update label="v5.0.6" description="8th February 2026">
|
||||
- Implement Atomic Resume Patching API for fine-grained resume updates, allowing partial, atomic updates to resumes via a new PATCH endpoint. [#2692](https://github.com/amruthpillai/reactive-resume/pull/2692)
|
||||
- The API endpoint `PUT /resume/{id}` now returns the updated resume object instead of void. Also, resume routing/services now use explicit DTOs for input and output schemas. [#2688](https://github.com/amruthpillai/reactive-resume/pull/2688)
|
||||
- Add error logging for API server errors (server-side only) to improve debugging and reliability.
|
||||
- Refactor and clean up imports/exports for clarity and maintainability.
|
||||
- Added `.devcontainer` configuration for improved contributor development environment.
|
||||
- Update dependencies to the latest versions.
|
||||
- Add build status badge and documentation link to README.
|
||||
- Sync latest translations from Crowdin (notably: French, other languages).
|
||||
- Other minor fixes and improvements.
|
||||
</Update>
|
||||
|
||||
|
||||
<Update label="v5.0.5" description="31st January 2026">
|
||||
- Implement Cover Letter functionality in the resume builder, allowing the user to create cover letters as custom sections. [(link)](/guides/adding-a-cover-letter)
|
||||
- Implement full-screen mode for the rich text editor in the resume builder, allowing the user to write in a more focused environment.
|
||||
- Implement a new custom section type: `summary`, which allows the user to add another "summary" like section to the resume.
|
||||
- Implement a `useFormBlocker` hook to prevent the user from closing a dialog while the form has unsaved changes.
|
||||
- Fix an issue where keywords spacing was not consistent in the interests section. [#2631](https://github.com/amruthpillai/reactive-resume/pull/2631)
|
||||
- Fix an issue where the AI connection test was not working correctly, also return appropriate error messages for AI provider issues.
|
||||
</Update>
|
||||
|
||||
<Update label="v5.0.4" description="28th January 2026">
|
||||
- Bring back Undo/Redo functionality in the resume builder for improved editing experience.
|
||||
- Arrange the sidebar builder dynamically based on the section type in each template. [#2564](https://github.com/amruthpillai/reactive-resume/pull/2603)
|
||||
- Remove extra spacing when proficiency is empty. [#2607](https://github.com/amruthpillai/reactive-resume/pull/2626)
|
||||
- Fix rendering in Pikachu template: conditionally render header and page picture using `isFirstPage`, and respect the `fullWidth` property for page layout.
|
||||
- Fixes to templates to improve layout and rendering consistency.
|
||||
- Fix GitHub OAuth login for users migrated from previous versions.
|
||||
- Improve communication with the printer service and reduce resource usage for better PDF generation reliability.
|
||||
- Update and sync translations from Crowdin (Afrikaans, Persian, Portuguese/Brazilian, and other languages).
|
||||
- Update translation sources and configuration.
|
||||
- Fix "empty" Git merge remnants in codebase.
|
||||
- Update package dependencies and fix self-hosting guide links in README.
|
||||
- Remove dead code, update screenshots, and add PWA (Progressive Web App) support.
|
||||
- Update links to PDF example files in documentation.
|
||||
- Other bug fixes and minor improvements (#2542, #2573, #2598).
|
||||
</Update>
|
||||
|
||||
<Update label="v5.0.3" description="25th January 2026">
|
||||
- Implement the ability to print Free-Form PDFs which do not have a fixed page height, allowing the user to fit the content on a page as they see fit. [(link)](/guides/selecting-page-format)
|
||||
- Allow the user to override the default endpoint for all AI providers, not just Ollama. Also display the default endpoint for each provider in the AI settings page.
|
||||
- Updated the chip input component, to allow the user to add, edit, remove or reorder keywords for skills and other sections.
|
||||
- Improved RTL support across the app, thanks to @obreo for the contribution. [(link)](https://github.com/amruthpillai/reactive-resume/pull/2583)
|
||||
- Updates to the translation configuration to remove line numbers from the translation files, as this was causing unnecessary diffs.
|
||||
- Updated the video on the homepage to be lighter and faster to load, while still maintaining the same quality.
|
||||
- Increased the screenshot TTL for resumes, to avoid regenerating screenshots unnecessarily unless the resume has been updated recently (in the last hour).
|
||||
- Update dependencies and translations to the latest versions.
|
||||
</Update>
|
||||
|
||||
<Update label="v5.0.2" description="24th January 2026">
|
||||
- Added an agent skill `skills/resume-builder` for agentic AI assistants to build resumes for Reactive Resume through conversational AI.
|
||||
- Added a new guide on how to fit content on a page, to avoid issues when exporting to PDF. [(link)](/guides/fitting-content-on-a-page)
|
||||
- Display an alert when the content is too tall for a page, to help the user fit the content on a page.
|
||||
- Fix an issue with the Ditgar template, where the page was not respecting the `fullWidth` setting.
|
||||
- Updated the JSON Schema to conform to a proper format.
|
||||
- Updated the Discord Server invite link to a new one.
|
||||
- Updated dependencies to the latest versions.
|
||||
</Update>
|
||||
|
||||
<Update label="v5.0.1" description="23th January 2026">
|
||||
- Updated translations from Crowdin.
|
||||
- Added a Community Spotlight section to the documentation.
|
||||
- Remove `-r require-metadata` from the Dockerfile as it was not needed.
|
||||
- Fixed inconsistencies in the docker compose examples in the documentation.
|
||||
- Fixed an issue with usernames not allowing hyphens in them.
|
||||
- Fixed issues with the printer service, when using the `getResumeScreenshot` or `printResumeAsPDF` endpoints.
|
||||
</Update>
|
||||
|
||||
<Update label="v5.0.0" description="22th January 2026">
|
||||
This has been a major overhaul from the previous version of Reactive Resume. The app has been completely redesigned and rebuilt from scratch, to be more intuitive and user-friendly.
|
||||
|
||||
**Here are some of the key changes from the previous version:**
|
||||
- 2 new templates: _Ditgar_ and _Lapras_
|
||||
- Authentication via Passkeys
|
||||
- New user interface and refreshed design
|
||||
- API Access and Reference Documentation
|
||||
- AI Integration with OpenAI, Google, Anthropic, and Ollama
|
||||
- A better font selector, with real-time preview of the font
|
||||
- More comprehensive documentation, with guides for specific features
|
||||
- An improved templates gallery, with clear overview of each template
|
||||
- Ability to import resumes from PDF or DOCX (requires AI Integration)
|
||||
- Reliable server infrastructure, with improved performance and scalability
|
||||
- Ability to move items between sections and pages, for better organization
|
||||
- A more powerful CSS Editor, with better autocomplete and syntax highlighting
|
||||
- Extended Custom Sections to include a type, for better customization and organization
|
||||
- Ability to choose the kind of icons to display for level indicators (stars, circles, custom icons, etc.)
|
||||
- Ability to resize how wide/narrow the sidebar should be on the resume, or convert a page to be full width
|
||||
|
||||
There's still a lot more that I'm forgetting, but I'm sure you'll find out soon enough as you explore the new version. I hope you enjoy building your resume now, better than ever. If you have any feedback, please feel free to [contact me](https://amruthpillai.com/#contact) or [open an issue](https://github.com/amruthpillai/reactive-resume/issues) on GitHub.
|
||||
</Update>
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: "Spotlight"
|
||||
description: "A showcase of articles, videos, and social media posts from the Reactive Resume community"
|
||||
---
|
||||
|
||||
<Note>
|
||||
Have you created something that makes use of Reactive Resume, or spread the word about it in your own way? I'd love to feature it here! Send me an email at [hello@amruthpillai.com](mailto:hello@amruthpillai.com) and I'll make sure to add it to this page.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Articles
|
||||
|
||||
A collection of blog posts and articles written by the community about Reactive Resume.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
{/* Add article cards here */}
|
||||
|
||||
{/* Example:
|
||||
<Card title="Article Title" icon="newspaper" href="https://example.com/article">
|
||||
A brief description of the article.
|
||||
</Card>
|
||||
*/}
|
||||
</CardGroup>
|
||||
|
||||
<Info>
|
||||
No articles have been featured yet. Be the first to contribute!
|
||||
</Info>
|
||||
|
||||
---
|
||||
|
||||
## Videos
|
||||
|
||||
Video tutorials, reviews, and walkthroughs created by the community.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
{/* Add video cards here */}
|
||||
|
||||
{/* Example:
|
||||
<Card title="Video Title" icon="youtube" href="https://youtube.com/watch?v=...">
|
||||
A brief description of the video.
|
||||
</Card>
|
||||
*/}
|
||||
</CardGroup>
|
||||
|
||||
<Info>
|
||||
No videos have been featured yet. Be the first to contribute!
|
||||
</Info>
|
||||
|
||||
---
|
||||
|
||||
## Social Media
|
||||
|
||||
Posts and threads from social media platforms sharing experiences with Reactive Resume.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
{/* Add social media cards here */}
|
||||
|
||||
{/* Example:
|
||||
<Card title="Post Title" icon="twitter" href="https://twitter.com/...">
|
||||
A brief description of the post.
|
||||
</Card>
|
||||
*/}
|
||||
</CardGroup>
|
||||
|
||||
<Info>
|
||||
No social media posts have been featured yet. Be the first to contribute!
|
||||
</Info>
|
||||
@@ -49,7 +49,7 @@ flowchart TD
|
||||
|
||||
Database["PostgreSQL"]
|
||||
Storage["File System OR S3-Compatible Storage"]
|
||||
Printer["Gotenberg"]
|
||||
Printer["Printer (Browserless/Chromium)"]
|
||||
|
||||
Router --> ORPCClient
|
||||
Query --> ORPCClient
|
||||
@@ -366,14 +366,14 @@ const title = t`Welcome`;
|
||||
<Card
|
||||
title="GitHub Discussions"
|
||||
icon="comments"
|
||||
href="https://github.com/AmruthPillai/Reactive-Resume/discussions"
|
||||
href="https://github.com/amruthpillai/reactive-resume/discussions"
|
||||
>
|
||||
Ask questions and discuss ideas with the community.
|
||||
</Card>
|
||||
<Card
|
||||
title="GitHub Issues"
|
||||
icon="bug"
|
||||
href="https://github.com/AmruthPillai/Reactive-Resume/issues"
|
||||
href="https://github.com/amruthpillai/reactive-resume/issues"
|
||||
>
|
||||
Report bugs or request features.
|
||||
</Card>
|
||||
|
||||
@@ -20,7 +20,7 @@ This guide walks you through setting up Reactive Resume for local development. W
|
||||
<Steps>
|
||||
<Step title="Clone the Repository">
|
||||
```bash
|
||||
git clone https://github.com/AmruthPillai/Reactive-Resume.git
|
||||
git clone https://github.com/amruthpillai/reactive-resume.git
|
||||
cd Reactive-Resume
|
||||
```
|
||||
</Step>
|
||||
@@ -47,7 +47,7 @@ This guide walks you through setting up Reactive Resume for local development. W
|
||||
This starts the following infrastructure services:
|
||||
- **PostgreSQL** — Database (port 5432)
|
||||
- **SeaweedFS** — S3-compatible storage (port 8333)
|
||||
- **Gotenberg** — PDF generation service (port 4000)
|
||||
- **Printer** — PDF and screenshot generation service (port 4000)
|
||||
- **Mailpit** — Email testing server (SMTP on port 1025, UI on port 8025)
|
||||
|
||||
<Tip>
|
||||
@@ -66,6 +66,10 @@ This guide walks you through setting up Reactive Resume for local development. W
|
||||
# Server
|
||||
APP_URL=http://localhost:3000
|
||||
|
||||
# Printer (required for local development)
|
||||
PRINTER_APP_URL=http://host.docker.internal:3000
|
||||
PRINTER_ENDPOINT=ws://localhost:4000?token=1234567890
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
|
||||
|
||||
@@ -79,16 +83,13 @@ This guide walks you through setting up Reactive Resume for local development. W
|
||||
S3_BUCKET=reactive-resume
|
||||
S3_FORCE_PATH_STYLE=true
|
||||
|
||||
# PDF Printer (required for local development)
|
||||
PRINTER_APP_URL=http://host.docker.internal:3000
|
||||
|
||||
# Email (Mailpit for local development)
|
||||
SMTP_HOST=localhost
|
||||
SMTP_PORT=1025
|
||||
```
|
||||
|
||||
<Note>
|
||||
**PDF Generation Note**: The `PRINTER_APP_URL` variable is required when running Reactive Resume outside of Docker while the Gotenberg PDF service is running inside Docker (which is the case when using `compose.dev.yml`). Gotenberg needs to reach your local app to render resumes for PDF generation. Since Docker containers cannot access `localhost` on your host machine directly, you must set `PRINTER_APP_URL` to `http://host.docker.internal:3000`. This special hostname allows Docker containers to communicate with services running on your host machine.
|
||||
**PDF Generation Note**: The `PRINTER_APP_URL` variable is required when running Reactive Resume outside of Docker while the printer service is running inside Docker (which is the case when using `compose.dev.yml`). The printer needs to reach your local app to render resumes for PDF generation. Since Docker containers cannot access `localhost` on your host machine directly, you must set `PRINTER_APP_URL` to `http://host.docker.internal:3000`. This special hostname allows Docker containers to communicate with services running on your host machine.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
@@ -135,8 +136,6 @@ Here are the most commonly used scripts during development:
|
||||
|---------|-------------|
|
||||
| `pnpm run db:generate` | Generate migration files from schema changes |
|
||||
| `pnpm run db:migrate` | Apply pending migrations |
|
||||
| `pnpm run db:push` | Push schema changes directly (dev only) |
|
||||
| `pnpm run db:pull` | Pull schema from existing database |
|
||||
| `pnpm run db:studio` | Open Drizzle Studio (database GUI) |
|
||||
|
||||
### Internationalization
|
||||
@@ -149,7 +148,7 @@ Here are the most commonly used scripts during development:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `pnpm run docs` | Start the Mintlify docs development server |
|
||||
| `pnpm run docs:dev` | Start the Mintlify docs development server |
|
||||
|
||||
---
|
||||
|
||||
@@ -300,13 +299,13 @@ pnpm run typecheck
|
||||
|
||||
```bash
|
||||
docker compose -f compose.dev.yml logs seaweedfs
|
||||
docker compose -f compose.dev.yml logs seaweedfs-create-bucket
|
||||
docker compose -f compose.dev.yml logs seaweedfs_create_bucket
|
||||
```
|
||||
|
||||
If the bucket wasn't created, restart the bucket creation service:
|
||||
|
||||
```bash
|
||||
docker compose -f compose.dev.yml restart seaweedfs-create-bucket
|
||||
docker compose -f compose.dev.yml restart seaweedfs_create_bucket
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
@@ -323,30 +322,6 @@ pnpm run typecheck
|
||||
pnpm run typecheck
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Uploaded images not appearing in PDF exports">
|
||||
When running in development mode (Docker services + app on host), images you upload to your resume may not appear in PDF exports. This is because the printer service runs inside Docker and cannot access URLs that resolve to `localhost` on your host machine.
|
||||
|
||||
**Why this happens:**
|
||||
|
||||
The app running on your host uploads images to SeaweedFS (S3 storage) using `localhost:8333`. When Gotenberg tries to fetch these images to render the PDF, it cannot resolve `localhost` the same way your host machine does — they're on different networks.
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Run the entire stack in Docker** (recommended for testing PDF exports):
|
||||
|
||||
Use the production `compose.yml` instead of `compose.dev.yml` to run all services, including the app, on the same Docker network:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
This ensures all services can communicate with each other seamlessly.
|
||||
|
||||
2. **Use publicly accessible images**:
|
||||
|
||||
If you need to test PDF exports while developing on the host, use images hosted on publicly accessible URLs (e.g., images already hosted online) instead of uploading local files. The printer service can fetch these without network issues.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
---
|
||||
@@ -364,7 +339,7 @@ pnpm run typecheck
|
||||
<Card
|
||||
title="GitHub Repository"
|
||||
icon="github"
|
||||
href="https://github.com/AmruthPillai/Reactive-Resume"
|
||||
href="https://github.com/amruthpillai/reactive-resume"
|
||||
>
|
||||
View the source code and contribute to the project.
|
||||
</Card>
|
||||
|
||||
@@ -95,7 +95,7 @@ If your language is not listed in the Crowdin project, you can request it to be
|
||||
|
||||
To request a new language:
|
||||
|
||||
1. Go to the [GitHub Issues](https://github.com/AmruthPillai/Reactive-Resume/issues) page
|
||||
1. Go to the [GitHub Issues](https://github.com/amruthpillai/reactive-resume/issues) page
|
||||
2. Click **New Issue**
|
||||
3. Select the appropriate template or create a blank issue
|
||||
4. Title it something like: "Add [Language Name] to Reactive Resume"
|
||||
@@ -147,7 +147,7 @@ Translations submitted on Crowdin are synced to the codebase periodically. Once
|
||||
<Card
|
||||
icon="github"
|
||||
title="GitHub Issues"
|
||||
href="https://github.com/AmruthPillai/Reactive-Resume/issues"
|
||||
href="https://github.com/amruthpillai/reactive-resume/issues"
|
||||
>
|
||||
Report issues or request new languages.
|
||||
</Card>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"seo": {
|
||||
"indexing": "all",
|
||||
"metatags": {
|
||||
"canonical": "https://docs.rxresu.me"
|
||||
"canonical": "https://docs.rxresu.me",
|
||||
"og:image": "https://rxresu.me/opengraph/banner.jpg"
|
||||
}
|
||||
},
|
||||
"fonts": {
|
||||
@@ -26,6 +27,7 @@
|
||||
"navigation": {
|
||||
"tabs": [
|
||||
{
|
||||
"icon": "book",
|
||||
"tab": "Documentation",
|
||||
"groups": [
|
||||
{
|
||||
@@ -33,8 +35,8 @@
|
||||
"pages": [
|
||||
"getting-started/index",
|
||||
"getting-started/quickstart",
|
||||
"guides/accessing-the-previous-version",
|
||||
"getting-started/changelog"
|
||||
"guides/checking-service-status",
|
||||
"guides/accessing-the-previous-version"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -51,6 +53,10 @@
|
||||
"pages": [
|
||||
"guides/creating-your-first-resume",
|
||||
"guides/choosing-a-template",
|
||||
"guides/selecting-page-format",
|
||||
"guides/moving-items-between-sections",
|
||||
"guides/fitting-content-on-a-page",
|
||||
"guides/adding-a-cover-letter",
|
||||
"guides/exporting-your-resume",
|
||||
"guides/sharing-your-resume-publicly",
|
||||
"guides/using-private-notes",
|
||||
@@ -63,16 +69,26 @@
|
||||
},
|
||||
{
|
||||
"group": "Integrations",
|
||||
"pages": ["guides/using-the-api", "guides/using-ai", "guides/json-resume-schema"]
|
||||
"pages": [
|
||||
"guides/using-the-api",
|
||||
"guides/using-the-patch-api",
|
||||
"guides/using-the-mcp-server",
|
||||
"guides/using-ai",
|
||||
"guides/json-resume-schema"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Self-hosting",
|
||||
"pages": ["guides/self-hosting-with-docker"]
|
||||
"group": "Self-Hosting",
|
||||
"pages": ["self-hosting/docker", "self-hosting/examples", "self-hosting/sso", "self-hosting/migration"]
|
||||
},
|
||||
{
|
||||
"group": "Contributing",
|
||||
"pages": ["contributing/architecture", "contributing/development", "contributing/translations"]
|
||||
},
|
||||
{
|
||||
"group": "Community",
|
||||
"pages": ["community/spotlight"]
|
||||
},
|
||||
{
|
||||
"group": "Legal",
|
||||
"pages": ["legal/license", "legal/privacy-policy", "legal/terms-of-service"]
|
||||
@@ -80,6 +96,12 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"icon": "clock",
|
||||
"tab": "Changelog",
|
||||
"pages": ["changelog/index"]
|
||||
},
|
||||
{
|
||||
"icon": "code",
|
||||
"tab": "API Reference",
|
||||
"openapi": "/spec.json"
|
||||
}
|
||||
@@ -94,7 +116,7 @@
|
||||
{
|
||||
"icon": "github",
|
||||
"anchor": "Source Code",
|
||||
"href": "https://github.com/AmruthPillai/Reactive-Resume"
|
||||
"href": "https://github.com/amruthpillai/reactive-resume"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -107,7 +129,7 @@
|
||||
"links": [
|
||||
{
|
||||
"label": "Support",
|
||||
"href": "https://github.com/AmruthPillai/Reactive-Resume/issues"
|
||||
"href": "https://github.com/amruthpillai/reactive-resume/issues"
|
||||
}
|
||||
],
|
||||
"primary": {
|
||||
@@ -122,7 +144,7 @@
|
||||
"footer": {
|
||||
"socials": {
|
||||
"website": "https://rxresu.me",
|
||||
"github": "https://github.com/AmruthPillai/Reactive-Resume"
|
||||
"github": "https://github.com/amruthpillai/reactive-resume"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
title: "Changelog"
|
||||
description: "List of all notable changes and updates to Reactive Resume"
|
||||
rss: true
|
||||
---
|
||||
|
||||
<Update label="v5.0.0" description="23rd January 2026">
|
||||
## New Features
|
||||
- Brand new UI/UX and refreshed design.
|
||||
</Update>
|
||||
@@ -4,7 +4,7 @@ description: "Welcome to the documentation for Reactive Resume, a free and open-
|
||||
---
|
||||
|
||||
<Frame>
|
||||
<img src="/images/banner.jpg" alt="Reactive Resume Banner" style={{ borderRadius: '0.75rem' }} />
|
||||
<img src="/images/getting-started/banner.webp" alt="Reactive Resume Banner" />
|
||||
</Frame>
|
||||
|
||||
## What is Reactive Resume?
|
||||
@@ -28,9 +28,13 @@ description: "Welcome to the documentation for Reactive Resume, a free and open-
|
||||
|
||||
## Key Features
|
||||
|
||||
<Frame>
|
||||
<img src="/images/getting-started/infographic.webp" alt="An infographic of the major features of Reactive Resume" />
|
||||
</Frame>
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Completely Free & Open Source" icon="code-branch">
|
||||
Reactive Resume is licensed under MIT. You can use it for free, modify it, and even host your own instance. The entire codebase is available on [GitHub](https://github.com/AmruthPillai/Reactive-Resume).
|
||||
Reactive Resume is licensed under MIT. You can use it for free, modify it, and even host your own instance. The entire codebase is available on [GitHub](https://github.com/amruthpillai/reactive-resume).
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Multiple Templates" icon="grid-2">
|
||||
@@ -94,13 +98,13 @@ Reactive Resume is built with modern web technologies:
|
||||
## Community & Support
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="GitHub" icon="github" href="https://github.com/AmruthPillai/Reactive-Resume">
|
||||
<Card title="GitHub" icon="github" href="https://github.com/amruthpillai/reactive-resume">
|
||||
Star the repo, report issues, and contribute to the project.
|
||||
</Card>
|
||||
<Card title="Reddit" icon="reddit" href="https://reddit.com/r/reactiveresume">
|
||||
Join our Reddit community to get help and connect with other users.
|
||||
</Card>
|
||||
<Card title="Discord" icon="discord" href="https://discord.gg/hzwkZbyvUW">
|
||||
<Card title="Discord" icon="discord" href="https://discord.gg/aSyA5ZSxpb">
|
||||
Join our Discord server to get help and connect with other users.
|
||||
</Card>
|
||||
<Card title="Sponsor" icon="heart" href="https://opencollective.com/reactive-resume">
|
||||
|
||||
@@ -73,7 +73,7 @@ Before you begin, ensure you have the following installed:
|
||||
<Steps>
|
||||
<Step title="Clone the Repository">
|
||||
```bash
|
||||
git clone https://github.com/AmruthPillai/Reactive-Resume.git
|
||||
git clone https://github.com/amruthpillai/reactive-resume.git
|
||||
cd Reactive-Resume
|
||||
```
|
||||
</Step>
|
||||
@@ -112,7 +112,7 @@ Before you begin, ensure you have the following installed:
|
||||
This will start:
|
||||
- **PostgreSQL** — Database for storing user data and resumes
|
||||
- **SeaweedFS** — S3-compatible storage for file uploads
|
||||
- **Gotenberg** — PDF generation service
|
||||
- **Printer** — PDF and screenshot generation service (browserless/chromium)
|
||||
- **Reactive Resume** — The main application
|
||||
</Step>
|
||||
|
||||
@@ -133,7 +133,7 @@ Here's what each service in the stack does:
|
||||
|---------|------|-------------|
|
||||
| `postgres` | 5432 | PostgreSQL database for storing all application data |
|
||||
| `seaweedfs` | 8333 | S3-compatible object storage for file uploads |
|
||||
| `gotenberg` | 4000 | Headless Chrome service for PDF generation |
|
||||
| `printer` | 4000 | Headless Chromium service for PDF and screenshot generation |
|
||||
| `app` | 3000 | The main Reactive Resume application |
|
||||
|
||||
### Health Checks
|
||||
@@ -159,15 +159,13 @@ Here's a complete list of environment variables you can configure:
|
||||
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@host:5432/db` |
|
||||
| `AUTH_SECRET` | Secret key for authentication | Generate with `openssl rand -base64 32` |
|
||||
| `APP_URL` | Public URL of your Application | `https://rxresu.me` |
|
||||
| `GOTENBERG_ENDPOINT` | URL of the Gotenberg PDF service | `http://gotenberg:4000` |
|
||||
| `PRINTER_ENDPOINT` | URL of the printer service | `http://printer:3000` |
|
||||
|
||||
### Optional Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PRINTER_APP_URL` | Public URL for Gotenberg to access the Application | — |
|
||||
| `GOTENBERG_USERNAME` | Gotenberg Basic Auth Username (if enabled) | — |
|
||||
| `GOTENBERG_PASSWORD` | Gotenberg Basic Auth Password (if enabled) | — |
|
||||
| `PRINTER_APP_URL` | Public URL for the printer to access the Application | — |
|
||||
| `GOOGLE_CLIENT_ID` | Google OAuth Client ID | — |
|
||||
| `GOOGLE_CLIENT_SECRET` | Google OAuth Client Secret | — |
|
||||
| `GITHUB_CLIENT_ID` | GitHub OAuth Client ID | — |
|
||||
@@ -193,12 +191,18 @@ Here's a complete list of environment variables you can configure:
|
||||
| `S3_BUCKET` | S3 Bucket Name | — |
|
||||
| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 (set `true` for MinIO/SeaweedFS) | `false` |
|
||||
| `FLAG_DEBUG_PRINTER` | Used for debugging the printer route | `false` |
|
||||
| `FLAG_DISABLE_SIGNUP` | Disables new user signups | `false` |
|
||||
| `FLAG_DISABLE_SIGNUPS` | Disables new user signups | `false` |
|
||||
| `FLAG_DISABLE_EMAIL_AUTH` | Disables email/password login (SSO only) | `false` |
|
||||
| `FLAG_DISABLE_IMAGE_PROCESSING` | Disables image processing | `false` |
|
||||
|
||||
> **Note:** Some variables are only required for using related features (OAuth, SMTP, S3, etc.) and can be left unset if unused.
|
||||
|
||||
<Note>
|
||||
**Hybrid Setup Note**: The `PRINTER_APP_URL` variable is required when running Reactive Resume outside of Docker while the Gotenberg PDF service is running inside Docker. In this scenario, Gotenberg needs to reach your local app to render resumes for PDF generation. Since Docker containers cannot access `localhost` on your host machine directly, you must set `PRINTER_APP_URL` to `http://host.docker.internal:3000`. This special hostname allows Docker containers to communicate with services running on your host machine.
|
||||
**Hybrid Setup Note**: The `PRINTER_APP_URL` variable is required when running Reactive Resume outside of Docker while the printer service is running inside Docker. In this scenario, the printer needs to reach your local app to render resumes for PDF generation. Since Docker containers cannot access `localhost` on your host machine directly, you must set `PRINTER_APP_URL` to `http://host.docker.internal:3000`. This special hostname allows Docker containers to communicate with services running on your host machine.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
**Alternative Printer Options**: If you don't want to use browserless, you can use any headless Chrome/Chromium instance with its remote debugging port open. For example, run `chromium --remote-debugging-port=9222` and point `PRINTER_ENDPOINT` to that instance.
|
||||
</Note>
|
||||
|
||||
---
|
||||
@@ -223,5 +227,5 @@ Here's a complete list of environment variables you can configure:
|
||||
</CardGroup>
|
||||
|
||||
<Note>
|
||||
**Having trouble?** Check our [GitHub Issues](https://github.com/AmruthPillai/Reactive-Resume/issues) or reach out via [email](mailto:hello@amruthpillai.com).
|
||||
**Having trouble?** Check our [GitHub Issues](https://github.com/amruthpillai/reactive-resume/issues) or reach out via [email](mailto:hello@amruthpillai.com).
|
||||
</Note>
|
||||
|
||||
@@ -62,4 +62,4 @@ If you'd like to move your resumes to the latest version of Reactive Resume, you
|
||||
|
||||
## Questions or issues?
|
||||
|
||||
If you encounter any problems accessing v4 or have questions about migrating your resumes, feel free to open an issue on [GitHub](https://github.com/AmruthPillai/Reactive-Resume/issues).
|
||||
If you encounter any problems accessing v4 or have questions about migrating your resumes, feel free to open an issue on [GitHub](https://github.com/amruthpillai/reactive-resume/issues).
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: "Adding a cover letter"
|
||||
description: "Learn how to create a cover letter as a custom section in Reactive Resume, format it professionally, and place it on a separate page."
|
||||
---
|
||||
|
||||
A cover letter is a personalized document that accompanies your resume when applying for a job. It introduces you to the employer, highlights your relevant qualifications, and explains why you're a good fit for the position. Reactive Resume lets you create cover letters as custom sections, giving you full control over formatting and placement.
|
||||
|
||||
## Why include a cover letter?
|
||||
|
||||
- **Stand out**: A well-written cover letter demonstrates genuine interest in the position
|
||||
- **Personalize your application**: Tailor your message to each employer and role
|
||||
- **Explain gaps or transitions**: Address career changes or gaps in your resume
|
||||
- **Show communication skills**: Demonstrate your ability to write clearly and professionally
|
||||
|
||||
## Creating a cover letter section
|
||||
|
||||
<Steps>
|
||||
<Step title="Open the resume builder">
|
||||
Navigate to your resume and open it in the builder.
|
||||
</Step>
|
||||
|
||||
<Step title="Scroll to Custom Sections">
|
||||
In the left sidebar, scroll down to find the **Custom Sections** area at the bottom.
|
||||
</Step>
|
||||
|
||||
<Step title="Add a new custom section">
|
||||
Click <Badge>Add a new custom section</Badge> to open the section creation dialog.
|
||||
</Step>
|
||||
|
||||
<Step title="Select 'Cover Letter' as the type">
|
||||
In the dialog:
|
||||
- Enter a **Title** for your section (e.g., "Cover Letter" or "Cover Letter - [Company Name]")
|
||||
- Select **Cover Letter** from the **Type** dropdown
|
||||
- Click **Create** to add the section
|
||||
</Step>
|
||||
|
||||
<Step title="Add a cover letter item">
|
||||
In your new cover letter section, click <Badge>Add a new item</Badge> to create your cover letter.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Formatting your cover letter
|
||||
|
||||
A cover letter has two main fields:
|
||||
|
||||
### Recipient field
|
||||
|
||||
The **Recipient** field is where you enter the recipient's information. This typically includes:
|
||||
- Date (optional)
|
||||
- Hiring manager's name
|
||||
- Their job title
|
||||
- Company name
|
||||
- Company address
|
||||
|
||||
<Tip>
|
||||
Use line breaks to format the address block. For example:
|
||||
|
||||
```
|
||||
January 31, 2026
|
||||
|
||||
Jane Smith
|
||||
Senior Hiring Manager
|
||||
Acme Corporation
|
||||
123 Main Street, Suite 400
|
||||
New York, NY 10001
|
||||
```
|
||||
</Tip>
|
||||
|
||||
### Content field
|
||||
|
||||
The **Content** field is where you write the body of your cover letter. Include:
|
||||
- **Salutation**: "Dear Ms. Smith," or "Dear Hiring Team,"
|
||||
- **Opening paragraph**: State the position you're applying for and how you learned about it
|
||||
- **Body paragraphs**: Highlight relevant experience, skills, and achievements
|
||||
- **Closing paragraph**: Express enthusiasm and include a call to action
|
||||
- **Sign-off**: "Sincerely," followed by your name
|
||||
|
||||
<Info>
|
||||
The recipient and content are both rendered directly on your resume without a section header, giving your cover letter a clean, professional appearance.
|
||||
</Info>
|
||||
|
||||
## Using fullscreen mode for writing
|
||||
|
||||
For a distraction-free writing experience, use the fullscreen mode in the rich text editor:
|
||||
|
||||
<Steps>
|
||||
<Step title="Open the cover letter item">
|
||||
Click on your cover letter item in the sidebar to open the edit dialog.
|
||||
</Step>
|
||||
|
||||
<Step title="Expand the editor">
|
||||
In either the Recipient or Content field, click the **expand icon** (arrows pointing outward) in the bottom-right corner of the editor.
|
||||
</Step>
|
||||
|
||||
<Step title="Write in fullscreen">
|
||||
The editor will expand to nearly fill your screen, giving you a focused writing environment with all formatting tools available.
|
||||
</Step>
|
||||
|
||||
<Step title="Exit fullscreen">
|
||||
Click the **collapse icon** or press **Escape** to return to the normal view.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Formatting options
|
||||
|
||||
The rich text editor supports various formatting options for your cover letter:
|
||||
|
||||
- **Text styling**: Bold, italic, underline, strikethrough, highlight
|
||||
- **Headings**: H1 through H6 (though typically not needed in a cover letter)
|
||||
- **Alignment**: Left, center, right, or justified text
|
||||
- **Lists**: Bullet points and numbered lists
|
||||
- **Links**: Add hyperlinks to your portfolio or LinkedIn
|
||||
|
||||
## Moving your cover letter to a separate page
|
||||
|
||||
Cover letters are typically on their own page, separate from the resume content. To achieve this:
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a second page">
|
||||
In the right sidebar, go to the **Layout** section and add a new page to your resume.
|
||||
</Step>
|
||||
|
||||
<Step title="Move the cover letter section">
|
||||
Use the **Move to** feature to relocate your cover letter section to the second page.
|
||||
|
||||
<Tip>
|
||||
See [Moving items between sections](/guides/moving-items-between-sections) for detailed instructions on how to move sections between pages.
|
||||
</Tip>
|
||||
</Step>
|
||||
|
||||
<Step title="Verify the layout">
|
||||
Check the preview to ensure your cover letter appears on its own page with proper formatting.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Warning>
|
||||
When exporting as PDF, pages are rendered in order. If you want the cover letter first, place it on page 1 and move your resume content to subsequent pages.
|
||||
</Warning>
|
||||
|
||||
## Tips for effective cover letters
|
||||
|
||||
<Tip>
|
||||
**Keep it concise**: Aim for 250-400 words. Recruiters spend about one minute reading cover letters.
|
||||
</Tip>
|
||||
|
||||
<Tip>
|
||||
**Tailor each letter**: Customize your cover letter for each application. Reference specific job requirements and company values.
|
||||
</Tip>
|
||||
|
||||
<Tip>
|
||||
**Use the same styling**: Your cover letter will inherit the fonts and colors from your resume template, ensuring a cohesive look.
|
||||
</Tip>
|
||||
|
||||
<Tip>
|
||||
**Proofread carefully**: Spelling and grammar errors can disqualify your application. Review your letter before exporting.
|
||||
</Tip>
|
||||
|
||||
## Managing multiple cover letters
|
||||
|
||||
Since cover letters are custom sections, you can:
|
||||
|
||||
- Create multiple cover letter sections for different job applications
|
||||
- Name each section descriptively (e.g., "Cover Letter - Google", "Cover Letter - Meta")
|
||||
- Show or hide sections as needed when exporting
|
||||
- Duplicate sections to use as templates for new applications
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: "Checking Service Status"
|
||||
description: "Learn how to check the status of Reactive Resume's servers and what to do if the service is experiencing issues"
|
||||
---
|
||||
|
||||
## Status Page
|
||||
|
||||
You can monitor the health and availability of Reactive Resume's servers at any time by visiting our status page:
|
||||
|
||||
<Card title="Status Page" icon="signal" href="https://status.rxresu.me">
|
||||
View real-time server metrics including uptime, CPU usage, memory usage, and more.
|
||||
</Card>
|
||||
|
||||
The status page provides information such as:
|
||||
|
||||
- **Uptime**: How long the servers have been running without interruption
|
||||
- **CPU Usage**: Current processor utilization
|
||||
- **Memory Usage**: RAM consumption across services
|
||||
- **Response Times**: How quickly the servers are responding to requests
|
||||
|
||||
---
|
||||
|
||||
## What to Do If Servers Are Down
|
||||
|
||||
If you notice the servers are experiencing high load or downtime, here are some recommendations:
|
||||
|
||||
<Steps>
|
||||
<Step title="Check the status page">
|
||||
Visit [status.rxresu.me](https://status.rxresu.me) to confirm if there's an ongoing issue. The page will show you the current state of all services.
|
||||
</Step>
|
||||
|
||||
<Step title="Wait and try again later">
|
||||
If the servers are under heavy load, the best course of action is to wait a bit and try again later. Peak usage times can cause temporary slowdowns.
|
||||
|
||||
<Info>
|
||||
Reactive Resume is a **free, open-source service** used by thousands of people worldwide. During peak times, the servers may experience higher than usual load.
|
||||
</Info>
|
||||
</Step>
|
||||
|
||||
<Step title="Check for announcements">
|
||||
For major outages or planned maintenance, announcements may be posted on our [GitHub repository](https://github.com/amruthpillai/reactive-resume).
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
## A Note on Server Capacity
|
||||
|
||||
<Warning>
|
||||
Reactive Resume is a **free service** that runs on limited server resources. As an open-source project maintained by a single developer, it's not feasible to invest in powerful dedicated servers without community support.
|
||||
</Warning>
|
||||
|
||||
The reality is:
|
||||
|
||||
- **High demand**: Thousands of users rely on this service daily
|
||||
- **Limited resources**: As a free service, server capacity is constrained
|
||||
- **No corporate backing**: This isn't a venture-funded startup with unlimited cloud budgets
|
||||
|
||||
If you find Reactive Resume valuable and want to help keep the servers running smoothly (and maybe even help scale them up), please consider supporting the project.
|
||||
|
||||
---
|
||||
|
||||
## Support the Project
|
||||
|
||||
Your donations directly help cover server costs, improve infrastructure, and keep Reactive Resume free for everyone.
|
||||
|
||||
<Card title="Donate on Open Collective" icon="heart" href="https://opencollective.com/reactive-resume">
|
||||
Support Reactive Resume's development and server costs through Open Collective. Every contribution helps keep the service running.
|
||||
</Card>
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="One-time Donation" icon="gift">
|
||||
Make a single contribution of any amount to help with immediate server costs.
|
||||
</Card>
|
||||
<Card title="Recurring Support" icon="repeat">
|
||||
Become a backer with a monthly contribution to provide sustainable support.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
---
|
||||
|
||||
## Self-Hosting as an Alternative
|
||||
|
||||
If you need guaranteed uptime or want to avoid shared server limitations, you can always self-host Reactive Resume on your own infrastructure.
|
||||
|
||||
<Card title="Self-Hosting Guide" icon="server" href="/self-hosting/docker">
|
||||
Learn how to deploy Reactive Resume on your own servers using Docker.
|
||||
</Card>
|
||||
|
||||
Self-hosting gives you:
|
||||
|
||||
- **Full control** over your data and infrastructure
|
||||
- **Guaranteed availability** based on your own server capacity
|
||||
- **No shared resources** with other users
|
||||
@@ -13,42 +13,38 @@ Changing your resume template is simple and can be done at any time without losi
|
||||
<Step title="Open your resume in the builder">
|
||||
Navigate to your Dashboard and click on the resume you want to edit.
|
||||
|
||||
{/* TODO: Add screenshot of dashboard with resume cards */}
|
||||
<Frame caption="Your resumes dashboard showing all your resume cards">
|
||||
<img src="/images/choosing-template/dashboard-placeholder.jpg" alt="Dashboard showing resume cards" style={{ aspectRatio: "210/297" }} />
|
||||
<Frame caption="Screenshot of your resumes dashboard showing all your resume">
|
||||
<img src="/images/guides/choosing-a-template/screenshot-1.webp" alt="Screenshot of your resumes dashboard showing all your resume" />
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Open the right sidebar">
|
||||
In the resume builder, look for the right sidebar. This is where you'll find all the design and layout options.
|
||||
|
||||
{/* TODO: Add screenshot of builder with right sidebar visible */}
|
||||
<Frame caption="The resume builder with the right sidebar open">
|
||||
<img src="/images/choosing-template/builder-sidebar-placeholder.jpg" alt="Resume builder showing the right sidebar" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Navigate to the Template section">
|
||||
In the right sidebar, find and click on the **Template** section to expand it.
|
||||
|
||||
{/* TODO: Add screenshot of template section in sidebar */}
|
||||
<Frame caption="The Template section in the right sidebar">
|
||||
<img src="/images/choosing-template/template-section-placeholder.jpg" alt="Template section expanded in the sidebar" style={{ aspectRatio: "210/297" }} />
|
||||
<Frame caption="Screenshot of the template section in the right sidebar">
|
||||
<img src="/images/guides/choosing-a-template/screenshot-2.webp" alt="Screenshot of the template section in the right sidebar" />
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Select your new template">
|
||||
Browse through the available templates and click on the one you want to use. Your resume will instantly update to reflect the new design.
|
||||
|
||||
{/* TODO: Add screenshot showing template selection */}
|
||||
<Frame caption="Selecting a new template from the available options">
|
||||
<img src="/images/choosing-template/template-selection-placeholder.jpg" alt="Template selection dropdown or grid" style={{ aspectRatio: "210/297" }} />
|
||||
<Frame caption="Screenshot of selecting a new template from the gallery">
|
||||
<img src="/images/guides/choosing-a-template/screenshot-3.webp" alt="Screenshot of selecting a new template from the gallery" />
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Review and adjust">
|
||||
After changing the template, review your resume in the live preview. You may want to adjust spacing, colors, or layout to optimize for the new design.
|
||||
|
||||
<Frame caption="Screenshot of the resume with a new template applied">
|
||||
<img src="/images/guides/choosing-a-template/screenshot-4.webp" alt="Screenshot of the resume with a new template applied" />
|
||||
</Frame>
|
||||
|
||||
<Tip>
|
||||
Different templates may display your content differently. Some templates work better with shorter content, while others are designed to handle more detailed information.
|
||||
</Tip>
|
||||
@@ -88,55 +84,59 @@ Reactive Resume includes a variety of professionally designed templates. Each te
|
||||
All templates support the same features and sections. The difference is purely in how they present your information visually.
|
||||
</Info>
|
||||
|
||||
<Columns cols={2}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Frame caption="Azurill">
|
||||
<img src="/images/templates/azurill.jpg" alt="Azurill template preview" style={{ aspectRatio: "210/297" }} />
|
||||
<img src="/images/templates/azurill.webp" alt="Azurill template preview" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
|
||||
<Frame caption="Bronzor">
|
||||
<img src="/images/templates/bronzor.jpg" alt="Bronzor template preview" style={{ aspectRatio: "210/297" }} />
|
||||
<img src="/images/templates/bronzor.webp" alt="Bronzor template preview" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
|
||||
<Frame caption="Chikorita">
|
||||
<img src="/images/templates/chikorita.jpg" alt="Chikorita template preview" style={{ aspectRatio: "210/297" }} />
|
||||
<img src="/images/templates/chikorita.webp" alt="Chikorita template preview" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
|
||||
<Frame caption="Ditto">
|
||||
<img src="/images/templates/ditto.jpg" alt="Ditto template preview" style={{ aspectRatio: "210/297" }} />
|
||||
<img src="/images/templates/ditto.webp" alt="Ditto template preview" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
|
||||
<Frame caption="Ditgar">
|
||||
<img src="/images/templates/ditgar.webp" alt="Ditgar template preview" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
|
||||
<Frame caption="Gengar">
|
||||
<img src="/images/templates/gengar.jpg" alt="Gengar template preview" style={{ aspectRatio: "210/297" }} />
|
||||
<img src="/images/templates/gengar.webp" alt="Gengar template preview" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
|
||||
<Frame caption="Glalie">
|
||||
<img src="/images/templates/glalie.jpg" alt="Glalie template preview" style={{ aspectRatio: "210/297" }} />
|
||||
<img src="/images/templates/glalie.webp" alt="Glalie template preview" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
|
||||
<Frame caption="Kakuna">
|
||||
<img src="/images/templates/kakuna.jpg" alt="Kakuna template preview" style={{ aspectRatio: "210/297" }} />
|
||||
<img src="/images/templates/kakuna.webp" alt="Kakuna template preview" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
|
||||
<Frame caption="Lapras">
|
||||
<img src="/images/templates/lapras.jpg" alt="Lapras template preview" style={{ aspectRatio: "210/297" }} />
|
||||
<img src="/images/templates/lapras.webp" alt="Lapras template preview" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
|
||||
<Frame caption="Leafish">
|
||||
<img src="/images/templates/leafish.jpg" alt="Leafish template preview" style={{ aspectRatio: "210/297" }} />
|
||||
<img src="/images/templates/leafish.webp" alt="Leafish template preview" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
|
||||
<Frame caption="Onyx">
|
||||
<img src="/images/templates/onyx.jpg" alt="Onyx template preview" style={{ aspectRatio: "210/297" }} />
|
||||
<img src="/images/templates/onyx.webp" alt="Onyx template preview" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
|
||||
<Frame caption="Pikachu">
|
||||
<img src="/images/templates/pikachu.jpg" alt="Pikachu template preview" style={{ aspectRatio: "210/297" }} />
|
||||
<img src="/images/templates/pikachu.webp" alt="Pikachu template preview" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
|
||||
<Frame caption="Rhyhorn">
|
||||
<img src="/images/templates/rhyhorn.jpg" alt="Rhyhorn template preview" style={{ aspectRatio: "210/297" }} />
|
||||
<img src="/images/templates/rhyhorn.webp" alt="Rhyhorn template preview" style={{ aspectRatio: "210/297" }} />
|
||||
</Frame>
|
||||
</Columns>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -6,18 +6,10 @@ description: "Learn how to create an account on Reactive Resume and get started
|
||||
<Steps>
|
||||
<Step title="Visit the homepage">
|
||||
Head over to [https://rxresu.me](https://rxresu.me) and click on the <Badge>Get Started</Badge> button.
|
||||
|
||||
<Frame>
|
||||
<img src="/images/creating-an-account/get-started-button.png" alt="Get Started button on homepage" />
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Navigate to the sign up page">
|
||||
You should see a link that says <Badge>Don't have an account? Create one now →</Badge>. Click on that link to go to the sign up page and you should see a form like this:
|
||||
|
||||
<Frame>
|
||||
<img src="/images/creating-an-account/create-account-link.png" alt="Create account link on login page" />
|
||||
</Frame>
|
||||
You should see a link that says <Badge>Don't have an account? Create one now →</Badge>. Click on that link to go to the sign up page and you should see a form.
|
||||
</Step>
|
||||
|
||||
<Step title="Fill in your details">
|
||||
@@ -27,11 +19,7 @@ description: "Learn how to create an account on Reactive Resume and get started
|
||||
- **Email Address**: A valid email address you have access to.
|
||||
- **Username**: Choose a unique username (this will be used in your public resume URLs)
|
||||
- **Password**: Create a strong password
|
||||
|
||||
<Frame>
|
||||
<img src="/images/creating-an-account/sign-up-form.png" alt="Sign up form" />
|
||||
</Frame>
|
||||
|
||||
|
||||
<Warning>
|
||||
Make sure to choose a username you're happy with, as it will be part of your public resume URL (e.g., `rxresu.me/your-username/resume-slug`).
|
||||
</Warning>
|
||||
@@ -62,10 +50,6 @@ description: "Learn how to create an account on Reactive Resume and get started
|
||||
- Create your first resume
|
||||
- Import an existing resume
|
||||
- Manage your account settings
|
||||
|
||||
<Frame>
|
||||
<img src="/images/creating-an-account/dashboard.png" alt="Reactive Resume dashboard" />
|
||||
</Frame>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
---
|
||||
title: "Fitting content on a page"
|
||||
description: "Learn how to reorganize and format your resume content to fit within a single page, whether you're using A4 or Letter format."
|
||||
---
|
||||
|
||||
When your resume content overflows the page, Reactive Resume displays a warning just below the page. This guide explains how to reorganize and adjust your content so everything fits within your chosen page format.
|
||||
|
||||
<Frame caption="Screenshot of the overflow warning message in the resume builder">
|
||||
<img src="/images/guides/fitting-content-on-a-page/screenshot-1.webp" alt="Screenshot of the overflow warning message in the resume builder" />
|
||||
</Frame>
|
||||
|
||||
<Warning>
|
||||
While Reactive Resume supports multi-page resumes, each page has a fixed height based on your chosen format (A4 or Letter). If a single page's content exceeds this height, parts of your resume may be rendered improperly when printed or exported.
|
||||
</Warning>
|
||||
|
||||
## Quick fixes
|
||||
|
||||
Here are several ways to fit your content within a page, ordered from simplest to most involved.
|
||||
|
||||
### 1. Switch to Free-Form format
|
||||
|
||||
If you don't plan on printing your resume, the simplest solution is to switch to **Free-Form** format. Free-Form creates a single continuous page with no height limit, eliminating overflow concerns entirely.
|
||||
|
||||
With Free-Form:
|
||||
|
||||
- Your entire resume renders as one seamless document
|
||||
- No awkward page breaks to manage
|
||||
- ATS parsers and AI scanners can still read your content perfectly
|
||||
- You focus on content, not arbitrary page constraints
|
||||
|
||||
To switch formats, go to the **Page** section in the right sidebar and change the **Format** to Free-Form. For more details, see [Selecting the right page format](/guides/selecting-page-format).
|
||||
|
||||
<Tip>
|
||||
Since most resumes are viewed digitally today, Free-Form is often the best choice unless you specifically need to print physical copies.
|
||||
</Tip>
|
||||
|
||||
### 2. Shorten text blocks
|
||||
|
||||
Long paragraphs take up space without adding proportional value. Review each section and cut ruthlessly:
|
||||
|
||||
- **Use bullet points** instead of paragraphs. Bullets are easier to scan and take less vertical space.
|
||||
- **Remove filler words.** "Was responsible for managing" becomes "Managed."
|
||||
- **Focus on impact.** Keep measurable achievements; cut generic descriptions.
|
||||
- **Limit bullets per entry.** Three to five bullets per job is usually enough.
|
||||
|
||||
<Tip>
|
||||
Read each bullet point and ask: "Does this help me get an interview?" If not, cut it.
|
||||
</Tip>
|
||||
|
||||
### 3. Use multi-column layouts
|
||||
|
||||
Some sections work better in multiple columns, especially lists of short items.
|
||||
|
||||
In the left sidebar, find the section you want to adjust, click on the section heading (not an item), and change the **Columns** setting.
|
||||
|
||||
<Frame caption="Screenshot of the columns setting for a section">
|
||||
<img src="/images/guides/fitting-content-on-a-page/screenshot-2.webp" alt="Screenshot of the columns setting for a section" />
|
||||
</Frame>
|
||||
|
||||
Good candidates for multi-column layouts:
|
||||
|
||||
| Section | Recommended Columns |
|
||||
|---------|---------------------|
|
||||
| Skills | > 2 columns |
|
||||
| Languages | > 3 columns |
|
||||
| Interests | > 2 columns |
|
||||
| Profiles | > 3 columns |
|
||||
| Certifications (if brief) | > 2 columns |
|
||||
|
||||
<Info>
|
||||
Multi-column layouts work best for sections with short, uniform items. Sections with long descriptions (like Experience or Projects) usually work better in a single column.
|
||||
</Info>
|
||||
|
||||
### 4. Move items to another page
|
||||
|
||||
If you have more content than fits on one page, move less important items to page two. This keeps your first page focused on your most relevant experience.
|
||||
|
||||
Use the **Move to** feature to relocate items:
|
||||
|
||||
1. Open the item's dropdown menu (three-dot icon)
|
||||
2. Hover over **Move to**
|
||||
3. Select the destination page
|
||||
|
||||
For detailed instructions, see [Moving items between sections](/guides/moving-items-between-sections).
|
||||
|
||||
<Tip>
|
||||
Keep your most recent and relevant experience on page one. Move older positions or less critical sections (like older projects or volunteer work) to subsequent pages.
|
||||
</Tip>
|
||||
|
||||
### 5. Adjust layout and design settings
|
||||
|
||||
The right sidebar contains settings that control how much space your content uses. Small adjustments here can make a big difference.
|
||||
|
||||
Open the right sidebar and explore these options:
|
||||
|
||||
| Setting | Where to find it | Effect |
|
||||
|---------|------------------|--------|
|
||||
| **Font size** | Typography | Smaller fonts fit more text per line and per page |
|
||||
| **Line height** | Typography | Tighter line spacing reduces vertical space |
|
||||
| **Margins** | Page | Smaller margins give you more usable area |
|
||||
| **Section gaps** | Page | Reducing gaps between sections saves space |
|
||||
| **Sidebar width** | Layout | Adjusting the sidebar ratio can balance content better |
|
||||
| **Picture size** | Picture (left sidebar) | A smaller photo leaves more room for text |
|
||||
|
||||
<Info>
|
||||
**Reducing font size is the best option** when you need to fit more content while keeping A4 or Letter format. Reducing body font from 11pt to 10.5pt (or even 10pt) can free up significant space while remaining readable. The editor supports 0.1pt increments, so you can fine-tune precisely.
|
||||
</Info>
|
||||
|
||||
### 6. Hide less important sections
|
||||
|
||||
If you're still short on space, consider hiding sections that aren't essential for your target role:
|
||||
|
||||
- **Interests** — Nice to have, but rarely a deciding factor
|
||||
- **References** — "Available upon request" is assumed; you don't need to list them
|
||||
- **Older certifications** — Keep only those relevant to the job
|
||||
- **Volunteer work** — Include only if it strengthens your application
|
||||
|
||||
To hide a section, click on the section heading in the left sidebar and toggle the **Hidden** switch.
|
||||
|
||||
## Finding the right balance
|
||||
|
||||
If you need to stick with A4 or Letter format, start with content changes (steps 2-4) before adjusting design settings (steps 5-6). The best resumes fit their content naturally rather than forcing everything into a cramped layout.
|
||||
|
||||
Try this order:
|
||||
|
||||
1. Consider switching to Free-Form if printing isn't required
|
||||
2. Cut unnecessary text first
|
||||
3. Reorganize with columns where appropriate
|
||||
4. Move secondary content to page two if needed
|
||||
5. Fine-tune font size and spacing last
|
||||
|
||||
<Tip>
|
||||
Use the live preview to see changes as you make them. Small adjustments add up—reducing font size by 0.5pt combined with slightly smaller margins can recover enough space for several lines of content.
|
||||
</Tip>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Content still overflows after trying everything
|
||||
|
||||
If you've tried all the above and content still overflows:
|
||||
|
||||
- **Re-evaluate what's essential.** Every item should earn its place. Cut aggressively.
|
||||
- **Try a different template.** Some templates are more space-efficient than others.
|
||||
|
||||
### The preview looks different from the PDF
|
||||
|
||||
The PDF export matches the preview exactly. If they appear different, try:
|
||||
|
||||
- Refreshing the page
|
||||
- Checking that all fonts have loaded
|
||||
- Ensuring your browser zoom is at 100%
|
||||
|
||||
### I made the font too small and now it's hard to read
|
||||
|
||||
Resume fonts should stay between 9pt and 12pt for body text. If you've gone below 9pt to fit content, you're trying to include too much. Go back to step 1 and cut more content instead.
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "Moving items between sections"
|
||||
description: "Learn how to move resume items from one section to another, or to a different page, to better organize your content and split lengthy sections across multiple pages."
|
||||
---
|
||||
|
||||
If you have a long work history or extensive project list, you may want to split items across multiple pages or reorganize them into different sections. The **Move to** feature lets you do exactly that—quickly relocate any item to another section or page with just a few clicks.
|
||||
|
||||
## Why move items?
|
||||
|
||||
- **Split lengthy sections**: If your Experience section spans more than one page, you can move older roles to a custom section on page 2.
|
||||
- **Reorganize content**: Move a project from "Projects" to a custom "Open Source" section, or relocate a skill to a different grouping.
|
||||
- **Fine-tune page layout**: Control exactly which items appear on which page for a polished, balanced resume.
|
||||
|
||||
## How to move an item
|
||||
|
||||
<Steps>
|
||||
<Step title="Open the resume builder">
|
||||
Navigate to your resume and open it in the builder.
|
||||
|
||||
<Info>
|
||||
Make sure you have at least one item in a section (e.g., an experience entry, project, or skill) before proceeding.
|
||||
</Info>
|
||||
</Step>
|
||||
|
||||
<Step title="Locate the item you want to move">
|
||||
In the left sidebar, find the section containing the item you want to relocate. Click on the section to expand it and view all items.
|
||||
|
||||
<Frame caption="Screenshot of the left sidebar with an expanded section containing multiple items">
|
||||
<img src="/images/guides/moving-items-between-sections/screenshot-1.webp" alt="Screenshot of the left sidebar with an expanded section containing multiple items" />
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Open the item dropdown menu">
|
||||
Each item has a **dropdown menu** (three-dot icon or chevron) on the right side. Click this icon to reveal the available actions.
|
||||
|
||||
<Frame caption="Screenshot of the dropdown menu icon on a section item">
|
||||
<img src="/images/guides/moving-items-between-sections/screenshot-2.webp" alt="Screenshot of the dropdown menu icon on a section item" />
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Hover over 'Move to'">
|
||||
In the dropdown menu, hover over or click the <Badge>Move to</Badge> option. This will open a submenu showing all available destinations.
|
||||
</Step>
|
||||
|
||||
<Step title="Select a destination">
|
||||
The submenu displays available destinations organized by:
|
||||
|
||||
- **Existing sections** of the same type (e.g., other Experience sections)
|
||||
- **Pages** where you can place the item
|
||||
- **Custom sections** if any exist
|
||||
|
||||
Click on your desired destination to move the item there.
|
||||
|
||||
<Frame caption="Screenshot of the 'Move to' submenu with destination options">
|
||||
<img src="/images/guides/moving-items-between-sections/screenshot-3.webp" alt="Screenshot of the 'Move to' submenu with destination options" />
|
||||
</Frame>
|
||||
|
||||
<Tip>
|
||||
If a custom section of the same type doesn't exist on your target page, Reactive Resume will **automatically create one** for you. This makes it easy to split sections across pages without manual setup.
|
||||
</Tip>
|
||||
</Step>
|
||||
|
||||
<Step title="Verify the move">
|
||||
After selecting a destination, the item will be moved immediately. You can verify by:
|
||||
|
||||
- Checking the destination section in the left sidebar
|
||||
- Looking at the resume preview to see where the item now appears
|
||||
|
||||
<Frame caption="Screenshot of the item in its new location">
|
||||
<img src="/images/guides/moving-items-between-sections/screenshot-4.webp" alt="Screenshot of the item in its new location" />
|
||||
</Frame>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Example: Splitting Work Experience across pages
|
||||
|
||||
A common use case is when your work history is too long to fit on a single page. Here's how to handle it:
|
||||
|
||||
<Steps>
|
||||
<Step title="Identify items to move">
|
||||
Review your Experience section and decide which roles should appear on page 1 (typically your most recent and relevant positions) and which can go on page 2.
|
||||
</Step>
|
||||
|
||||
<Step title="Move older positions">
|
||||
For each older position you want to relocate:
|
||||
|
||||
1. Open the item's dropdown menu
|
||||
2. Click <Badge>Move to</Badge>
|
||||
3. Select **Page 2** (or the appropriate page)
|
||||
|
||||
<Info>
|
||||
If no Experience section exists on page 2, a new custom section will be created automatically with the same type, so your formatting stays consistent.
|
||||
</Info>
|
||||
</Step>
|
||||
|
||||
<Step title="Review the result">
|
||||
Check the resume preview to ensure:
|
||||
|
||||
- Page 1 contains your most important, recent roles
|
||||
- Page 2 continues with your earlier experience
|
||||
- The section headings and styling remain consistent
|
||||
|
||||
{/* TODO: Add screenshot showing a resume with Experience split across page 1 and page 2 */}
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Tips for organizing multi-page resumes
|
||||
|
||||
<Tip>
|
||||
**Keep it logical**: Maintain chronological order within each page. Move complete job entries rather than splitting a single role across pages.
|
||||
</Tip>
|
||||
|
||||
<Tip>
|
||||
**Use custom section names**: After moving items to a new custom section, you can rename it (e.g., "Earlier Experience" or "Additional Projects") to provide context for recruiters.
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
Moving items reorganizes your content but doesn't affect the data itself. You can always move items back or to different sections as needed.
|
||||
</Warning>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### I don't see the "Move to" option
|
||||
|
||||
Make sure you're clicking the dropdown menu on a **section item** (like an individual job or project), not the section header itself. The Move to feature is only available for items within sections.
|
||||
|
||||
### The destination I want isn't listed
|
||||
|
||||
The Move to submenu shows destinations compatible with the item type. For example, an Experience item can only be moved to other Experience-type sections. If you need to change an item's type entirely, you may need to recreate it in the desired section.
|
||||
|
||||
### My custom section wasn't created
|
||||
|
||||
If you're moving to a page that already has a section of the same type, the item will be added to that existing section rather than creating a new one. This is by design to avoid duplicate sections.
|
||||
@@ -0,0 +1,143 @@
|
||||
---
|
||||
title: "Selecting the right page format"
|
||||
description: "Learn about the three page format options in Reactive Resume—A4, Letter, and Free-Form—and choose the best one for your needs."
|
||||
---
|
||||
|
||||
Reactive Resume offers three page format options: **A4**, **Letter**, and **Free-Form**. Each format affects how your resume is rendered and exported as a PDF. This guide explains the differences and helps you choose the right format for your situation.
|
||||
|
||||
## Available formats
|
||||
|
||||
### A4
|
||||
|
||||
A4 is the international standard paper size used in most countries outside North America. When you select A4, your resume pages conform to these dimensions:
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Width | 210mm (794px) |
|
||||
| Height | 297mm (1123px) |
|
||||
|
||||
Choose A4 if you're applying to jobs internationally or in regions that use the metric system.
|
||||
|
||||
### Letter
|
||||
|
||||
Letter is the standard paper size in the United States and Canada. When you select Letter, your resume pages conform to these dimensions:
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Width | 216mm (816px) |
|
||||
| Height | 279mm (1056px) |
|
||||
|
||||
Choose Letter if you're applying to jobs in North America.
|
||||
|
||||
### Free-Form
|
||||
|
||||
Free-Form is a modern format designed for digital-first resumes. Instead of conforming to a physical page size, Free-Form produces a **single continuous page** with no height limit. The width matches A4 (210mm), but the height extends as needed to fit all your content.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Width | 210mm (794px) |
|
||||
| Height | Unlimited |
|
||||
|
||||
<Info>
|
||||
With Free-Form, there are no page breaks. Your entire resume renders as one seamless document, regardless of how much content you have.
|
||||
</Info>
|
||||
|
||||
## Why Free-Form exists
|
||||
|
||||
The reality of modern job applications is that **nobody prints resumes anymore**. Your resume is almost always viewed digitally—on a screen, in an applicant tracking system (ATS), or parsed by AI-powered screening tools.
|
||||
|
||||
When your resume is processed digitally:
|
||||
|
||||
- **ATS parsers** extract text content regardless of page dimensions
|
||||
- **AI scanners** analyze the full document as a single unit
|
||||
- **Recruiters** scroll through PDFs on their screens rather than printing them
|
||||
- **PDF parsing tools** read the entire content regardless of page height
|
||||
|
||||
Since physical page constraints no longer matter for most use cases, Free-Form lets you focus on content rather than worrying about fitting everything into arbitrary page limits.
|
||||
|
||||
<Tip>
|
||||
If you don't plan on printing your resume, Free-Form is often the simplest choice. You never have to worry about content overflowing or awkward page breaks.
|
||||
</Tip>
|
||||
|
||||
## How to change your page format
|
||||
|
||||
<Steps>
|
||||
<Step title="Open your resume in the builder">
|
||||
Navigate to your Dashboard and click on the resume you want to edit.
|
||||
</Step>
|
||||
|
||||
<Step title="Open the right sidebar">
|
||||
In the resume builder, look for the right sidebar on the right side of the screen.
|
||||
</Step>
|
||||
|
||||
<Step title="Navigate to the Page section">
|
||||
In the right sidebar, find and click on the **Page** section to expand it.
|
||||
</Step>
|
||||
|
||||
<Step title="Select your format">
|
||||
Find the **Format** dropdown and select your preferred option: A4, Letter, or Free-Form.
|
||||
|
||||
<Frame caption="Screenshot of the Format dropdown in the Page section">
|
||||
<img src="/images/guides/selecting-page-format/screenshot-1.webp" alt="Screenshot of the Format dropdown in the Page section" />
|
||||
</Frame>
|
||||
</Step>
|
||||
|
||||
<Step title="Review the preview">
|
||||
Your resume preview updates immediately to reflect the new format. Check that your content displays correctly.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Choosing the right format
|
||||
|
||||
Use this quick reference to decide which format fits your needs:
|
||||
|
||||
| Situation | Recommended Format |
|
||||
|-----------|-------------------|
|
||||
| Applying to jobs in North America | Letter |
|
||||
| Applying to jobs internationally | A4 |
|
||||
| Digital-only applications (no printing) | Free-Form |
|
||||
| Uploading to ATS or job portals | Free-Form |
|
||||
| Need to print physical copies | A4 or Letter |
|
||||
| Long resume with lots of content | Free-Form |
|
||||
| Traditional industries (law, finance) | A4 or Letter |
|
||||
|
||||
<Warning>
|
||||
If you switch from Free-Form to A4 or Letter, your content will be split across multiple pages. Review the result carefully to ensure page breaks don't occur in awkward places.
|
||||
</Warning>
|
||||
|
||||
## Example PDFs
|
||||
|
||||
Download these sample resumes to see how different formats affect the final PDF output:
|
||||
|
||||
<CardGroup cols={1}>
|
||||
<Card arrow horizontal title="A4 Format Example" icon="file-pdf" href="https://github.com/user-attachments/files/24849548/a4.pdf">
|
||||
A sample resume in A4 format showing traditional page breaks and constraints.
|
||||
</Card>
|
||||
<Card arrow horizontal title="Free-Form Example" icon="file-lines" href="https://github.com/user-attachments/files/24849552/free-form.pdf">
|
||||
A sample resume in Free-Form format showing a continuous single-page layout.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Does Free-Form work with ATS systems?">
|
||||
Yes. ATS systems parse the text content of your PDF, not the page dimensions. Free-Form resumes are fully compatible with applicant tracking systems.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Can I switch formats after creating my resume?">
|
||||
Absolutely. You can change the format at any time from the Page section in the right sidebar. Your content is preserved—only the layout changes.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="What if a job posting asks for a 'one-page resume'?">
|
||||
If the employer specifically requests a one-page resume and expects a traditional format, use A4 or Letter and ensure your content fits within a single page. See [Fitting content on a page](/guides/fitting-content-on-a-page) for tips.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Does Free-Form affect the file size?">
|
||||
Slightly. A longer single page may produce a marginally larger PDF than a paginated version of the same content, but the difference is negligible for typical resume lengths.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Which format should I use for LinkedIn?">
|
||||
LinkedIn doesn't display uploaded resumes in their original format—it extracts the content. Free-Form works perfectly for LinkedIn uploads.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -3,6 +3,14 @@ title: "Setting up passkeys"
|
||||
description: "Learn how to register passkeys (WebAuthn) to sign in securely using biometrics or your device PIN"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
<strong>Passkeys Temporarily Disabled:</strong> Passkey registration and sign-in are currently unavailable in Reactive Resume due to an upstream issue with our authentication provider. We are closely monitoring the situation and will re-enable passkeys once it is resolved.
|
||||
|
||||
For more details and technical updates, see the <a href="https://github.com/better-auth/better-auth/issues/7463" target="_blank" rel="noopener">GitHub issue #7463</a>.
|
||||
</Warning>
|
||||
|
||||
---
|
||||
|
||||
<Steps>
|
||||
<Step title="Sign in to the dashboard">
|
||||
Head over to [https://rxresu.me](https://rxresu.me) and sign in with your account credentials.
|
||||
|
||||
@@ -11,6 +11,10 @@ The **Custom CSS** panel lets you write your own CSS rules to change how your re
|
||||
- **Auto-save**: there is no “Save” button—your CSS is saved automatically when it changes.
|
||||
- **Scoped styling (mostly)**: to avoid affecting the rest of the app, your CSS is *usually* scoped to the resume preview.
|
||||
|
||||
<Frame caption="Screenshot of the Custom CSS section in the right sidebar">
|
||||
<img src="/images/guides/using-custom-css/screenshot-1.webp" alt="Screenshot of the Custom CSS section in the right sidebar" />
|
||||
</Frame>
|
||||
|
||||
## Where to find it
|
||||
|
||||
In the resume builder, open the **right sidebar** and select **Custom CSS** (the `CSS` section).
|
||||
@@ -54,30 +58,30 @@ Example:
|
||||
|
||||
### Formatting tip (to avoid scoping edge-cases)
|
||||
|
||||
Keep each rule’s selector on **one line**, for example:
|
||||
Keep each rule's selector on **one line**, for example:
|
||||
|
||||
```css
|
||||
.page-section-experience .section-item-title { font-weight: 700; }
|
||||
```
|
||||
|
||||
Avoid splitting a selector across multiple lines unless you’ve confirmed it behaves as expected.
|
||||
Avoid splitting a selector across multiple lines unless you've confirmed it behaves as expected.
|
||||
|
||||
## Finding the right selectors
|
||||
|
||||
### Use autocomplete in the editor
|
||||
|
||||
In the Custom CSS editor, type a `.` (dot). You’ll get suggestions for commonly-used class selectors, including:
|
||||
In the Custom CSS editor, type a `.` (dot). You'll get suggestions for commonly-used class selectors, including:
|
||||
|
||||
- **Page-level**: `.page`, `.page-content`, `.page-header`, `.page-basics`, `.page-main`, `.page-sidebar`, `.page-picture`
|
||||
- **Section-level**: `.page-section`, `.section-content`
|
||||
- **Section types**: `.page-section-experience`, `.page-section-education`, `.page-section-projects`, etc.
|
||||
- **Generic item building blocks**: `.section-item`, `.section-item-header`, `.section-item-title`, `.section-item-description`, `.section-item-metadata`, `.section-item-link`, etc.
|
||||
- **Generic item building blocks**: `.section-item`, `.section-item-header`, `.section-item-title`, `.section-item-description`, `.section-item-metadata`, `.section-item-website`, etc.
|
||||
- **Per-section item selectors**: `.experience-item`, `.skills-item`, `.profiles-item`, etc.
|
||||
- **Template wrapper**: `.template-azurill`, `.template-onyx`, `.template-gengar`, etc.
|
||||
|
||||
### Stable “starter selectors”
|
||||
|
||||
If you’re not sure where to start, these are usually safe:
|
||||
If you're not sure where to start, these are usually safe:
|
||||
|
||||
- `.page` (the resume page container)
|
||||
- `.page-section` (each resume section)
|
||||
@@ -194,11 +198,11 @@ Example (tighten spacing without changing your layout settings):
|
||||
### 5) Style links like a “chip” (useful for project links)
|
||||
|
||||
```css
|
||||
.section-item-link a {
|
||||
.section-item-website a {
|
||||
display: inline-block;
|
||||
padding: 2pt 6pt;
|
||||
padding: 3pt 9pt;
|
||||
border-radius: 999pt;
|
||||
border: 1pt solid color-mix(in srgb, var(--page-primary-color) 45%, transparent);
|
||||
border: 2pt solid color-mix(in srgb, var(--page-primary-color) 45%, transparent);
|
||||
text-decoration: none;
|
||||
}
|
||||
```
|
||||
@@ -241,40 +245,25 @@ Example (tighten spacing without changing your layout settings):
|
||||
}
|
||||
```
|
||||
|
||||
### 9) Highlight a specific section (example: Experience)
|
||||
### 9) Highlight a specific section (example: Profiles)
|
||||
|
||||
```css
|
||||
.page-section-experience {
|
||||
background: color-mix(in srgb, var(--page-primary-color) 6%, transparent);
|
||||
.page-section-profiles {
|
||||
background: color-mix(in srgb, var(--page-primary-color) 10%, transparent);
|
||||
border-radius: 8pt;
|
||||
padding: 8pt;
|
||||
}
|
||||
```
|
||||
|
||||
### 10) Print-only adjustments (manually scoped)
|
||||
|
||||
```css
|
||||
@media print {
|
||||
.resume-preview-container .page {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
|
||||
.resume-preview-container a {
|
||||
text-decoration: none;
|
||||
}
|
||||
padding: 8pt 12pt;
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### My CSS doesn’t do anything
|
||||
### My CSS doesn't do anything
|
||||
|
||||
- Make sure **Enable** is turned on.
|
||||
- Prefer targeting `.page`, `.page-section`, and `.section-item` instead of `body`.
|
||||
- Use your browser devtools to **inspect** the preview and confirm the element/class names.
|
||||
|
||||
### My `@media` rules affect the whole app / don’t apply
|
||||
### My `@media` rules affect the whole app / don't apply
|
||||
|
||||
At-rules aren’t auto-scoped. Prefix selectors inside them with `.resume-preview-container` as shown above.
|
||||
At-rules aren't auto-scoped. Prefix selectors inside them with `.resume-preview-container` as shown above.
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ Think of it as a built-in notebook attached to each resume where you can jot dow
|
||||
|
||||
In the resume builder, open the **right sidebar** and select **Notes** from the available sections.
|
||||
|
||||
<Frame caption="Screenshot of the Notes section in the right sidebar">
|
||||
<img src="/images/guides/using-private-notes/screenshot-1.webp" alt="Screenshot of the Notes section in the right sidebar" />
|
||||
</Frame>
|
||||
|
||||
## Use cases
|
||||
|
||||
Private Notes are designed to help you stay organized during your job search. Here are some practical ways to use them:
|
||||
|
||||
@@ -43,7 +43,7 @@ description: "Learn how to create API keys and authenticate requests to the Reac
|
||||
</Info>
|
||||
|
||||
```bash
|
||||
curl "https://rxresu.me/api/openapi/resume/list" \
|
||||
curl "https://rxresu.me/api/openapi/resumes" \
|
||||
-H "x-api-key: YOUR_API_KEY"
|
||||
```
|
||||
</Step>
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
title: "Using the MCP Server"
|
||||
description: "Connect Reactive Resume to AI tools like Claude Desktop, Cursor, and Codex using the Model Context Protocol"
|
||||
---
|
||||
|
||||
The Reactive Resume MCP server lets you manage and modify your resumes through any MCP-compatible AI tool — Claude Desktop, Cursor, Codex, and more. It connects to the Reactive Resume API and exposes tools for listing, reading, and patching resumes using natural language.
|
||||
|
||||
## What is MCP?
|
||||
|
||||
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is a standard that lets LLM-powered tools connect to external services. Instead of being limited to the built-in chat UI, you can use any MCP client to interact with your resumes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
<Steps>
|
||||
<Step title="Create an API key">
|
||||
Head over to [https://rxresu.me](https://rxresu.me) (or your self-hosted instance), sign in, and navigate to **Settings → API Keys**. Click **Create a new API key**, give it a name, and copy the secret — it's only shown once.
|
||||
|
||||
For the full walkthrough, see [Using the API](/guides/using-the-api).
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Configuration
|
||||
|
||||
There are two ways to connect, depending on whether your MCP client supports the Streamable HTTP transport natively.
|
||||
|
||||
### Method 1: Streamable HTTP (recommended)
|
||||
|
||||
If your client supports the `url` field (e.g. **Cursor**), use this — no extra dependencies required:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"reactive-resume": {
|
||||
"url": "https://rxresu.me/mcp",
|
||||
"headers": {
|
||||
"x-api-key": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Method 2: mcp-remote
|
||||
|
||||
If your client only supports `command` / `args` (e.g. **Claude Desktop**), use [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) as a bridge. This requires [Node.js](https://nodejs.org) **20 or later**.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"reactive-resume": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"mcp-remote",
|
||||
"https://rxresu.me/mcp",
|
||||
"--header",
|
||||
"x-api-key:your-api-key"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Info>
|
||||
Replace `your-api-key` with the API key you created in the prerequisites step.
|
||||
</Info>
|
||||
|
||||
### Where to put the config
|
||||
|
||||
| Client | Config file |
|
||||
| --- | --- |
|
||||
| Cursor | `.cursor/mcp.json` in your project or home directory |
|
||||
| Claude Desktop | `claude_desktop_config.json` ([docs](https://modelcontextprotocol.io/docs/tools/claude-desktop)) |
|
||||
| Other MCP clients | Refer to the client's documentation |
|
||||
|
||||
## Self-Hosting
|
||||
|
||||
If you're running a self-hosted Reactive Resume instance, replace `https://rxresu.me/mcp` with your instance URL:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://resume.example.com/mcp",
|
||||
"headers": {
|
||||
"x-api-key": "your-api-key"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
The MCP server exposes the following tools:
|
||||
|
||||
| Tool | Description |
|
||||
| --- | --- |
|
||||
| `list_resumes` | List all resumes with IDs, names, tags, and status. Supports filtering by tags and sorting by last updated, creation date, or name |
|
||||
| `get_resume` | Get the full data of a specific resume by ID |
|
||||
| `create_resume` | Create a new, empty resume with a name and slug. Optionally pre-fill with sample data |
|
||||
| `duplicate_resume` | Create a copy of an existing resume with a new name and slug |
|
||||
| `patch_resume` | Apply JSON Patch (RFC 6902) operations to modify a resume's data |
|
||||
| `delete_resume` | Permanently delete a resume and all associated files. **Irreversible** |
|
||||
| `lock_resume` | Lock a resume to prevent edits, patches, and deletion |
|
||||
| `unlock_resume` | Unlock a previously locked resume to re-enable editing |
|
||||
| `export_resume_pdf` | Generate a PDF from the resume and return a download URL |
|
||||
| `get_resume_screenshot` | Get a visual preview of the resume's first page as a WebP image URL |
|
||||
| `get_resume_statistics` | Get view and download statistics for a resume |
|
||||
|
||||
## Available Resources
|
||||
|
||||
| Resource | Description |
|
||||
| --- | --- |
|
||||
| `resume://{id}` | The full resume data as a readable JSON resource. Lists all resumes and supports reading individual ones by ID |
|
||||
| `resume://schema` | The ResumeData JSON Schema — reference this to understand valid paths and value types for JSON Patch operations |
|
||||
|
||||
## Available Prompts
|
||||
|
||||
Prompts are pre-built workflows that provide the AI with structured instructions and context. Each prompt embeds the resume data and schema automatically.
|
||||
|
||||
| Prompt | Description |
|
||||
| --- | --- |
|
||||
| `build_resume` | Guide you step-by-step through building a resume from scratch — basics, summary, experience, education, skills, and design |
|
||||
| `improve_resume` | Review your resume and suggest concrete improvements to wording, impact, metrics, and structure |
|
||||
| `tailor_resume` | Adapt your resume to match a specific job description with keyword optimization and ATS targeting. Requires the job description as input |
|
||||
| `review_resume` | Get a structured, professional critique with a scorecard (1–10 across seven dimensions) and prioritized recommendations. **Read-only** — no changes are made |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
Once your MCP client is connected, you can use natural language to interact with your resumes:
|
||||
|
||||
### Browsing
|
||||
|
||||
- "List my resumes"
|
||||
- "Show me my resume named 'Software Engineer'"
|
||||
- "What skills are listed on my resume?"
|
||||
- "Show me the stats for my resume"
|
||||
|
||||
### Creating & Managing
|
||||
|
||||
- "Create a new resume called 'Frontend Engineer 2026'"
|
||||
- "Duplicate my 'Software Engineer' resume for a product manager role"
|
||||
- "Lock my finalized resume so it can't be accidentally edited"
|
||||
- "Delete my old draft resume"
|
||||
|
||||
### Editing
|
||||
|
||||
- "Update my name to Jane Doe"
|
||||
- "Change my headline to Senior Software Engineer"
|
||||
- "Add TypeScript to my skills with an Advanced proficiency level"
|
||||
- "Add a new experience entry for my role as Staff Engineer at Acme Corp from Jan 2024 to Present"
|
||||
- "Remove the third item from my skills section"
|
||||
|
||||
### Styling
|
||||
|
||||
- "Change the template to bronzor"
|
||||
- "Set the primary color to blue"
|
||||
- "Hide the interests section"
|
||||
|
||||
### Exporting
|
||||
|
||||
- "Export my resume as a PDF"
|
||||
- "Show me a screenshot of my resume"
|
||||
|
||||
### Using Prompts
|
||||
|
||||
- "Help me build my resume from scratch" (uses `build_resume`)
|
||||
- "Review my resume and give me a score" (uses `review_resume`)
|
||||
- "Improve the wording on my resume" (uses `improve_resume`)
|
||||
- "Tailor my resume for this job description: ..." (uses `tailor_resume`)
|
||||
|
||||
<Tip>
|
||||
The AI will use `get_resume` to inspect your current resume before making changes with `patch_resume`. This ensures the correct JSON paths are used.
|
||||
</Tip>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
| --- | --- |
|
||||
| "API error (401)" | Your API key is invalid or expired. Create a new one in **Settings → API Keys** |
|
||||
| "API error (404)" | The resume ID doesn't exist. Use `list_resumes` to find valid IDs |
|
||||
| "API error (403)" | The resume is locked. Unlock it in the Reactive Resume dashboard |
|
||||
| Connection refused | Check that the URL is correct and the instance is running |
|
||||
| "ReferenceError: File is not defined" when using `mcp-remote` | You're running Node.js 18. `mcp-remote` requires **Node.js 20 or later** — upgrade with `nvm use 20` or `nvm alias default 20` |
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: "Using the Patch API"
|
||||
description: "Learn how to partially update your resume using JSON Patch (RFC 6902) operations"
|
||||
---
|
||||
|
||||
The Patch API lets you make small, targeted changes to your resume without sending the entire data object. Instead of replacing the whole resume with a `PUT`, you send a list of **JSON Patch** operations that describe exactly what to change.
|
||||
|
||||
This is based on the [JSON Patch (RFC 6902)](https://datatracker.ietf.org/doc/html/rfc6902) standard.
|
||||
|
||||
## When to Use PATCH vs PUT
|
||||
|
||||
| Use case | Method |
|
||||
| --- | --- |
|
||||
| Update a single field (e.g., name, headline) | **PATCH** |
|
||||
| Add or remove an item in a section | **PATCH** |
|
||||
| Change template, colors, or fonts | **PATCH** |
|
||||
| Replace the entire resume data at once | **PUT** |
|
||||
|
||||
<Info>
|
||||
The PATCH endpoint only modifies the resume `data` (the JSONB column). To update top-level resume properties like `name`, `slug`, `tags`, or `isPublic`, use the existing `PUT /resume/{id}` endpoint.
|
||||
</Info>
|
||||
|
||||
## Authentication
|
||||
|
||||
All requests require your API key in the `x-api-key` header. See [Using the API](/guides/using-the-api) for how to create one.
|
||||
|
||||
<Info>
|
||||
If you're self-hosting, replace `https://rxresu.me` with your instance URL. The API is served under `/api/openapi`.
|
||||
</Info>
|
||||
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
PATCH /api/openapi/resume/{id}
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
The resume ID is taken from the URL path, so the request body only requires the `operations` array:
|
||||
|
||||
```json
|
||||
{
|
||||
"operations": [
|
||||
{ "op": "replace", "path": "/basics/name", "value": "Jane Doe" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Each operation is an object with the following properties:
|
||||
|
||||
| Property | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `op` | Yes | The operation to perform: `add`, `remove`, `replace`, `move`, `copy`, or `test` |
|
||||
| `path` | Yes | A JSON Pointer (RFC 6901) to the target location in the resume data |
|
||||
| `value` | For `add`, `replace`, `test` | The value to use for the operation |
|
||||
| `from` | For `move`, `copy` | A JSON Pointer to the source location |
|
||||
|
||||
## Examples
|
||||
|
||||
### Replace a Basic Field
|
||||
|
||||
Update the resume holder's name and headline:
|
||||
|
||||
```bash
|
||||
curl -X PATCH "https://rxresu.me/api/openapi/resume/YOUR_RESUME_ID" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"operations": [
|
||||
{ "op": "replace", "path": "/basics/name", "value": "Jane Doe" },
|
||||
{ "op": "replace", "path": "/basics/headline", "value": "Senior Software Engineer" }
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### Add an Experience Entry
|
||||
|
||||
Append a new item to the experience section:
|
||||
|
||||
```bash
|
||||
curl -X PATCH "https://rxresu.me/api/openapi/resume/YOUR_RESUME_ID" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"operations": [
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/sections/experience/items/-",
|
||||
"value": {
|
||||
"id": "a1b2c3d4-0000-0000-0000-000000000000",
|
||||
"hidden": false,
|
||||
"company": "Acme Corp",
|
||||
"position": "Staff Engineer",
|
||||
"location": "San Francisco, CA",
|
||||
"period": "Jan 2024 - Present",
|
||||
"website": { "url": "https://acme.example.com", "label": "Acme Corp" },
|
||||
"description": "<p>Leading the platform team.</p>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
<Tip>
|
||||
The path `/sections/experience/items/-` uses the special `-` index, which means "append to the end of the array".
|
||||
To insert at a specific position, use a numeric index like `/sections/experience/items/0` for the beginning.
|
||||
</Tip>
|
||||
|
||||
### Remove an Item from a Section
|
||||
|
||||
Remove the second skill (index `1`) from the skills section:
|
||||
|
||||
```bash
|
||||
curl -X PATCH "https://rxresu.me/api/openapi/resume/YOUR_RESUME_ID" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"operations": [
|
||||
{ "op": "remove", "path": "/sections/skills/items/1" }
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### Update Metadata (Template, Colors, Fonts)
|
||||
|
||||
Switch the template and update the primary color:
|
||||
|
||||
```bash
|
||||
curl -X PATCH "https://rxresu.me/api/openapi/resume/YOUR_RESUME_ID" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"operations": [
|
||||
{ "op": "replace", "path": "/metadata/template", "value": "bronzor" },
|
||||
{ "op": "replace", "path": "/metadata/design/colors/primary", "value": "rgba(37, 99, 235, 1)" }
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### Test-Then-Replace (Optimistic Concurrency)
|
||||
|
||||
The `test` operation checks that a value matches before proceeding. If the test fails, the entire patch is rejected. This is useful to avoid overwriting changes made by another client:
|
||||
|
||||
```bash
|
||||
curl -X PATCH "https://rxresu.me/api/openapi/resume/YOUR_RESUME_ID" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"operations": [
|
||||
{ "op": "test", "path": "/basics/name", "value": "Albert Einstein" },
|
||||
{ "op": "replace", "path": "/basics/name", "value": "Jane Doe" }
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
If `/basics/name` is not `"Albert Einstein"` at the time of the request, the entire patch will fail with a `400` error and no changes will be applied.
|
||||
|
||||
### Move an Item Within a Section
|
||||
|
||||
Move the first experience item to the third position:
|
||||
|
||||
```bash
|
||||
curl -X PATCH "https://rxresu.me/api/openapi/resume/YOUR_RESUME_ID" \
|
||||
-H "x-api-key: YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"operations": [
|
||||
{ "op": "move", "from": "/sections/experience/items/0", "path": "/sections/experience/items/2" }
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Status | Error Code | Description |
|
||||
| --- | --- | --- |
|
||||
| `400` | `INVALID_PATCH_OPERATIONS` | The operations are structurally invalid, target a non-existent path, or produce resume data that fails schema validation. |
|
||||
| `401` | `UNAUTHORIZED` | Missing or invalid API key. |
|
||||
| `404` | `NOT_FOUND` | The resume does not exist or does not belong to the authenticated user. |
|
||||
| `403` | `RESUME_LOCKED` | The resume is locked and cannot be modified. Unlock it first. |
|
||||
|
||||
<Warning>
|
||||
All operations in a single request are applied atomically. If any operation fails (including a `test`), none of the operations are applied.
|
||||
</Warning>
|
||||
|
||||
## Tips
|
||||
|
||||
- **Fetch first, then patch.** Use `GET /resume/{id}` to inspect the current structure before crafting your operations. This helps you target the correct paths and array indices.
|
||||
- **Use `test` for safety.** When updating a value you expect to be a specific value, combine `test` + `replace` to avoid accidentally overwriting concurrent changes.
|
||||
- **Batch related changes.** You can send multiple operations in a single request. They are applied in order, so later operations can depend on earlier ones.
|
||||
- **The `-` index appends.** When adding items to arrays, use `-` as the index (e.g., `/sections/skills/items/-`) to append to the end.
|
||||
|
Before Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 301 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
Before Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 299 KiB |
|
Before Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 203 KiB |
@@ -9,7 +9,7 @@ Reactive Resume is open-source software. The project is published under the **MI
|
||||
|
||||
If you are running a modified/self-hosted instance, you may have additional notices or third-party licenses that apply (for example, fonts, icons, or other bundled assets).
|
||||
|
||||
For the upstream repository, see: [AmruthPillai/Reactive-Resume](https://github.com/AmruthPillai/Reactive-Resume)
|
||||
For the upstream repository, see: [amruthpillai/reactive-resume](https://github.com/amruthpillai/reactive-resume)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -120,10 +120,10 @@ The Service does not include built-in behavioral advertising or third-party anal
|
||||
|
||||
We share information only as needed to provide the Service:
|
||||
|
||||
### PDF generation and screenshots (Gotenberg)
|
||||
When you export to PDF or request a screenshot, the Service sends a request to a configured **Gotenberg** endpoint to render a resume URL.
|
||||
### PDF generation and screenshots (Printer)
|
||||
When you export to PDF or request a screenshot, the Service sends a request to a configured printer endpoint (a headless Chromium browser) to render a resume URL.
|
||||
|
||||
Depending on your deployment, Gotenberg may be:
|
||||
Depending on your deployment, the printer may be:
|
||||
|
||||
- Self-hosted and controlled by the Service Operator, or
|
||||
- Operated by a third party (in which case the third party will process the resume content for rendering)
|
||||
|
||||
@@ -84,7 +84,7 @@ You represent that you have the rights necessary to upload and use any files and
|
||||
|
||||
## Exports (PDF) and Screenshots
|
||||
|
||||
When you request a PDF export or screenshot, the Service may use a rendering service (commonly **Gotenberg**) to load a resume URL and generate the output.
|
||||
When you request a PDF export or screenshot, the Service uses a printer service (a headless Chromium browser) to load a resume URL and generate the output.
|
||||
|
||||
Depending on the deployment, this rendering service may be operated by the Service Operator or a third party. You understand that resume content must be processed for rendering in order to provide the export/screenshot functionality.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Self-hosting with Docker"
|
||||
description: "A comprehensive guide to self-host Reactive Resume with Docker (Postgres + Gotenberg), including a detailed environment variable reference and troubleshooting tips."
|
||||
title: "Self-Hosting with Docker"
|
||||
description: "A comprehensive guide to self-host Reactive Resume with Docker (Postgres + Printer), including a detailed environment variable reference and troubleshooting tips."
|
||||
---
|
||||
|
||||
## Overview
|
||||
@@ -11,8 +11,8 @@ Reactive Resume can be self-hosted using Docker in a matter of minutes, and this
|
||||
<Card title="PostgreSQL">
|
||||
Stores accounts, resumes, and application data.
|
||||
</Card>
|
||||
<Card title="Gotenberg">
|
||||
Generates PDFs by rendering a special print route.
|
||||
<Card title="Printer">
|
||||
Generates PDFs and screenshots using a headless Chromium browser.
|
||||
</Card>
|
||||
<Card title="Email (optional)">
|
||||
SMTP for verification emails, password reset, etc. If not configured, emails are logged to the server console.
|
||||
@@ -34,7 +34,7 @@ You can pull the latest app image from:
|
||||
Docker Engine + Docker Compose plugin (or Docker Desktop).
|
||||
</Card>
|
||||
<Card title="Compute">
|
||||
2 vCPU / 2 GB RAM minimum (4 GB recommended if Postgres + Gotenberg run on the same host).
|
||||
2 vCPU / 2 GB RAM minimum (4 GB recommended if Postgres + Printer run on the same host).
|
||||
</Card>
|
||||
<Card title="Storage">
|
||||
Enough for Postgres + uploads (start with 10-20 GB and scale as needed).
|
||||
@@ -64,11 +64,9 @@ APP_URL="http://localhost:3000"
|
||||
PRINTER_APP_URL="http://host.docker.internal:3000"
|
||||
|
||||
# --- Printer ---
|
||||
GOTENBERG_ENDPOINT="http://gotenberg:3000"
|
||||
|
||||
# Gotenberg Authentication (Optional)
|
||||
# GOTENBERG_USERNAME=""
|
||||
# GOTENBERG_PASSWORD=""
|
||||
# If using browserless with token authentication, include the token as a query parameter:
|
||||
# PRINTER_ENDPOINT="ws://printer:3000?token=your-secret-token"
|
||||
PRINTER_ENDPOINT="ws://printer:3000"
|
||||
|
||||
# --- Database (PostgreSQL) ---
|
||||
DATABASE_URL="postgresql://postgres:postgres@postgres:5432/postgres"
|
||||
@@ -121,10 +119,8 @@ S3_FORCE_PATH_STYLE="false"
|
||||
|
||||
# --- Feature Flags ---
|
||||
FLAG_DEBUG_PRINTER="false"
|
||||
FLAG_DISABLE_SIGNUP="false"
|
||||
|
||||
# --- Others ---
|
||||
# GOOGLE_CLOUD_API_KEY=""
|
||||
FLAG_DISABLE_SIGNUPS="false"
|
||||
FLAG_DISABLE_EMAIL_AUTH="false"
|
||||
```
|
||||
</Step>
|
||||
|
||||
@@ -150,44 +146,45 @@ FLAG_DISABLE_SIGNUP="false"
|
||||
</Step>
|
||||
|
||||
<Step title="Create compose.yml">
|
||||
This setup runs Postgres + Gotenberg + Reactive Resume on a private Docker network.
|
||||
This setup runs Postgres + Printer + Reactive Resume on a private Docker network.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```yaml compose.yml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- postgres_data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:edge
|
||||
printer:
|
||||
image: ghcr.io/browserless/chromium:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4000:3000"
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
- CHROMIUM_AUTO_START=true
|
||||
- LIBREOFFICE_DISABLE_ROUTES=true
|
||||
- HEALTH=true
|
||||
- CONCURRENT=20
|
||||
- QUEUED=10
|
||||
# Optional: Set a token for authentication
|
||||
# - TOKEN=your-secret-token
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/pressure?token=your-secret-token"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
app:
|
||||
reactive-resume:
|
||||
image: amruthpillai/reactive-resume:latest
|
||||
# image: ghcr.io/amruthpillai/reactive-resume:latest
|
||||
restart: unless-stopped
|
||||
@@ -201,10 +198,10 @@ services:
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
gotenberg:
|
||||
printer:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\""]
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
@@ -215,6 +212,20 @@ volumes:
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
**Alternative Printer Options**: If you don't want to use browserless, you can also use a lightweight headless Chrome Docker image like `chromedp/headless-shell`:
|
||||
|
||||
```yaml
|
||||
chrome:
|
||||
image: chromedp/headless-shell:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9222:9222"
|
||||
```
|
||||
|
||||
Then set `PRINTER_ENDPOINT` to `http://chrome:9222` (or `http://localhost:9222` if running outside Docker Compose). This provides the same PDF/screenshot generation functionality with a smaller image footprint.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
Prefer pulling from Docker Hub? Keep <code>amruthpillai/reactive-resume:latest</code>. Prefer GHCR? Swap it to <code>ghcr.io/amruthpillai/reactive-resume:latest</code>.
|
||||
</Tip>
|
||||
@@ -254,7 +265,7 @@ docker compose logs -f reactive-resume
|
||||
<ul>
|
||||
<li><code>APP_URL</code></li>
|
||||
<li><code>DATABASE_URL</code></li>
|
||||
<li><code>GOTENBERG_ENDPOINT</code></li>
|
||||
<li><code>PRINTER_ENDPOINT</code></li>
|
||||
<li><code>AUTH_SECRET</code></li>
|
||||
</ul>
|
||||
</Card>
|
||||
@@ -272,12 +283,25 @@ docker compose logs -f reactive-resume
|
||||
<Accordion title="Server">
|
||||
- **`TZ`**: Sets the container timezone (affects logs and server-side timestamps). Recommended: `Etc/UTC`.
|
||||
- **`APP_URL`**: Canonical/public URL for your instance (used for absolute URLs, redirects, and auth flows). If behind a reverse proxy, set this to your public HTTPS URL (for example, `https://resume.example.com`).
|
||||
- **`PRINTER_APP_URL`** (optional): Overrides the base URL used when rendering the print route for Gotenberg. Defaults to `APP_URL`. Useful when Gotenberg must access the app via a different internal URL (for example, `http://host.docker.internal:3000`).
|
||||
- **`PRINTER_APP_URL`** (optional): Overrides the base URL used when rendering the print route for the printer. Defaults to `APP_URL`. Useful when the printer must access the app via a different internal URL (for example, `http://host.docker.internal:3000`).
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Printer (Gotenberg)">
|
||||
- **`GOTENBERG_ENDPOINT`**: Base URL where Reactive Resume reaches Gotenberg. In Compose: `http://gotenberg:3000`.
|
||||
- **`GOTENBERG_USERNAME`** / **`GOTENBERG_PASSWORD`** (optional): Use if Gotenberg is configured with Basic Auth (recommended only if Gotenberg is reachable outside your private network).
|
||||
<Accordion title="Printer">
|
||||
- **`PRINTER_ENDPOINT`**: Base URL where Reactive Resume reaches the printer service. In Compose: `http://printer:3000`. If using browserless with token authentication, include the token as a query parameter: `ws://printer:3000?token=your-secret-token`.
|
||||
|
||||
<Note>
|
||||
**Alternative to browserless**: You can use a lightweight headless Chrome Docker image like `chromedp/headless-shell`:
|
||||
|
||||
```yaml
|
||||
chrome:
|
||||
image: chromedp/headless-shell:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9222:9222"
|
||||
```
|
||||
|
||||
Set `PRINTER_ENDPOINT` to `http://chrome:9222` (in Docker Compose) or `http://localhost:9222` (if running externally). This provides the same PDF/screenshot generation with a smaller image footprint.
|
||||
</Note>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Database (PostgreSQL)">
|
||||
@@ -337,7 +361,9 @@ openssl rand -hex 32
|
||||
|
||||
<Accordion title="Feature Flags">
|
||||
- **`FLAG_DEBUG_PRINTER`**: Bypasses the printer-only access restriction (useful when debugging `/printer/{resumeId}`). Recommended: keep `"false"` in production.
|
||||
- **`FLAG_DISABLE_SIGNUP`**: Disables new signups (web app and server). Useful for private instances.
|
||||
- **`FLAG_DISABLE_SIGNUPS`**: Disables new signups (web app and server). Useful for private instances.
|
||||
- **`FLAG_DISABLE_EMAIL_AUTH`**: Disables email/password login entirely. Also disables email verification, forgot password, and reset password flows. Users can still sign up via social auth (Google/GitHub/Custom OAuth), unless FLAG_DISABLE_SIGNUPS is also set to true. Useful when only SSO is required.
|
||||
- **`FLAG_DISABLE_IMAGE_PROCESSING`**: Disables image processing. This is useful if you are using a machine with limited resources, like a Raspberry Pi.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -386,7 +412,7 @@ The Docker Compose configuration includes a health check that periodically calls
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\""]
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
@@ -439,10 +465,10 @@ A healthy response returns HTTP 200. Any other response (or a connection failure
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="PDF export fails / printing is stuck">
|
||||
- **Common cause**: Reactive Resume can't reach Gotenberg or Gotenberg can't reach your app.
|
||||
- **Common cause**: Reactive Resume can't reach the printer or the printer can't reach your app.
|
||||
- **Checks**:
|
||||
- `GOTENBERG_ENDPOINT` should usually be `http://gotenberg:3000` in Compose.
|
||||
- If you use `PRINTER_APP_URL="http://host.docker.internal:3000"`, ensure `extra_hosts: host-gateway` is present for Gotenberg.
|
||||
- `PRINTER_ENDPOINT` should usually be `http://printer:3000` in Compose.
|
||||
- If you use `PRINTER_APP_URL="http://host.docker.internal:3000"`, ensure `extra_hosts: host-gateway` is present for the printer service.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Uploads disappear after restart">
|
||||
@@ -461,4 +487,3 @@ A healthy response returns HTTP 200. Any other response (or a connection failure
|
||||
- **Fix**: Set `S3_FORCE_PATH_STYLE="true"` in your environment. This is required for most self-hosted S3-compatible services like MinIO, SeaweedFS, etc.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
---
|
||||
title: "Docker Compose Examples"
|
||||
description: "A collection of Docker Compose examples for different deployment scenarios. If you have a different setup that works for you, please share it by opening a pull request on GitHub."
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Every self-hosted setup is unique. You might be running on a single VPS, a Kubernetes cluster, behind Cloudflare Tunnel, or using a specific reverse proxy like Traefik or nginx. This page provides real-world Docker Compose configurations for various deployment scenarios to help you get started faster.
|
||||
|
||||
These examples go beyond the basic setup in the [Self-Hosting with Docker](/self-hosting/docker) guide, showing production-ready configurations with reverse proxies, SSL termination, and other common patterns.
|
||||
|
||||
<Info>
|
||||
**Help others by sharing your setup!** If you have a working configuration that isn't covered here, I'd love to include it. Simply [open a pull request](https://github.com/amruthpillai/reactive-resume) with your example added to this page. Your contribution helps the community and makes self-hosting easier for everyone.
|
||||
</Info>
|
||||
|
||||
---
|
||||
|
||||
## Docker with Traefik
|
||||
|
||||
This example uses [Traefik](https://traefik.io/) as a reverse proxy with automatic SSL certificate management via Let's Encrypt. Only the Reactive Resume app is exposed through Traefik—Postgres and the printer remain on an internal network.
|
||||
|
||||
<Tip>
|
||||
Traefik automatically discovers services via Docker labels and handles SSL certificates, making it ideal for setups where you want minimal configuration.
|
||||
</Tip>
|
||||
|
||||
```yaml compose-traefik.yml lines expandable
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.2
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--api.dashboard=true"
|
||||
- "--providers.docker=true"
|
||||
- "--providers.docker.exposedbydefault=false"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
|
||||
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
|
||||
- "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
|
||||
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
|
||||
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
|
||||
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- traefik_letsencrypt:/letsencrypt
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# Dashboard (optional, remove if not needed)
|
||||
- "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)"
|
||||
- "traefik.http.routers.traefik.entrypoints=websecure"
|
||||
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.routers.traefik.service=api@internal"
|
||||
- "traefik.http.routers.traefik.middlewares=auth"
|
||||
- "traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}"
|
||||
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_DB=postgres
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
printer:
|
||||
image: ghcr.io/browserless/chromium:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- QUEUED=10
|
||||
- HEALTH=true
|
||||
- CONCURRENT=5
|
||||
# Optional: Set a token for authentication
|
||||
# - TOKEN=your-secret-token
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/pressure?token=your-secret-token"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
reactive_resume:
|
||||
image: amruthpillai/reactive-resume:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- APP_URL=https://resume.${DOMAIN}
|
||||
- PRINTER_APP_URL=http://reactive_resume:3000
|
||||
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
|
||||
- PRINTER_ENDPOINT=http://printer:3000
|
||||
- AUTH_SECRET=${AUTH_SECRET}
|
||||
# Add other optional env vars as needed (SMTP, S3, OAuth, etc.)
|
||||
volumes:
|
||||
- reactive_resume_data:/app/data
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
printer:
|
||||
condition: service_healthy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.reactive-resume.rule=Host(`resume.${DOMAIN}`)"
|
||||
- "traefik.http.routers.reactive-resume.entrypoints=websecure"
|
||||
- "traefik.http.routers.reactive-resume.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.reactive-resume.loadbalancer.server.port=3000"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
networks:
|
||||
reactive_resume_network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
traefik_letsencrypt:
|
||||
postgres_data:
|
||||
reactive_resume_data:
|
||||
```
|
||||
|
||||
**Environment variables (`.env`):**
|
||||
|
||||
```bash .env
|
||||
DOMAIN="example.com"
|
||||
ACME_EMAIL="admin@example.com"
|
||||
POSTGRES_PASSWORD="your-secure-postgres-password"
|
||||
AUTH_SECRET="your-auth-secret-from-openssl-rand-hex-32"
|
||||
# Optional: Traefik dashboard auth (generate with: htpasswd -nb admin password)
|
||||
TRAEFIK_DASHBOARD_AUTH="admin:$$apr1$$..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker with nginx
|
||||
|
||||
This example uses [nginx](https://nginx.org/) as a reverse proxy with SSL certificates (you'll need to provide your own certificates or use certbot separately).
|
||||
|
||||
```yaml compose-nginx.yml lines expandable
|
||||
services:
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./certs:/etc/nginx/certs:ro
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
printer:
|
||||
image: ghcr.io/browserless/chromium:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- QUEUED=10
|
||||
- HEALTH=true
|
||||
- CONCURRENT=5
|
||||
# Optional: Set a token for authentication
|
||||
# - TOKEN=your-secret-token
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
|
||||
reactive_resume:
|
||||
image: amruthpillai/reactive-resume:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- APP_URL=https://resume.${DOMAIN}
|
||||
- PRINTER_APP_URL=http://reactive_resume:3000
|
||||
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres
|
||||
- PRINTER_ENDPOINT=http://printer:3000
|
||||
- AUTH_SECRET=${AUTH_SECRET}
|
||||
# Add other optional env vars as needed (SMTP, S3, OAuth, etc.)
|
||||
volumes:
|
||||
- reactive_resume_data:/app/data
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
printer:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
networks:
|
||||
reactive_resume_network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
reactive_resume_data:
|
||||
```
|
||||
|
||||
**nginx configuration (`nginx.conf`):**
|
||||
|
||||
```nginx nginx.conf lines expandable
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
upstream reactive_resume {
|
||||
server reactive_resume:3000;
|
||||
}
|
||||
|
||||
# Redirect HTTP to HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# HTTPS server
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name resume.example.com;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||
|
||||
# SSL configuration
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# Proxy settings
|
||||
location / {
|
||||
proxy_pass http://reactive_resume;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
# Timeouts for long-running requests (PDF generation)
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# Increase max body size for resume uploads
|
||||
client_max_body_size 10M;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Tip>
|
||||
For automatic SSL certificates with nginx, consider using [certbot](https://certbot.eff.org/) with the `--nginx` plugin, or a companion container like [nginx-proxy-acme](https://github.com/nginx-proxy/acme-companion).
|
||||
</Tip>
|
||||
|
||||
---
|
||||
|
||||
## Docker Swarm
|
||||
|
||||
This example demonstrates a production-grade Docker Swarm deployment with multiple replicas, health checks, rolling updates, and Traefik integration. It includes SeaweedFS for S3-compatible storage and a PostgreSQL database with custom configuration.
|
||||
|
||||
<Tip>
|
||||
Docker Swarm is great for multi-node deployments where you need high availability and easy scaling. The app service is configured with 2 replicas and rolling update strategy.
|
||||
</Tip>
|
||||
|
||||
```yaml compose-swarm.yml lines expandable
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
volumes:
|
||||
- reactive_resume_postgres_data:/var/lib/postgresql
|
||||
environment:
|
||||
- POSTGRES_DB=$POSTGRES_DB
|
||||
- POSTGRES_USER=$POSTGRES_USER
|
||||
- POSTGRES_PASSWORD=$POSTGRES_PASSWORD
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
|
||||
printer:
|
||||
image: ghcr.io/browserless/chromium:latest
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
environment:
|
||||
- QUEUED=10
|
||||
- HEALTH=true
|
||||
- CONCURRENT=5
|
||||
# Optional: Set a token for authentication
|
||||
# - TOKEN=your-secret-token
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/pressure?token=your-secret-token"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
|
||||
seaweedfs:
|
||||
image: chrislusf/seaweedfs:latest
|
||||
command: server -s3 -filer -dir=/data -ip=0.0.0.0
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
volumes:
|
||||
- reactive_resume_seaweedfs_data:/data
|
||||
environment:
|
||||
- AWS_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
|
||||
- AWS_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:8888"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
|
||||
seaweedfs_create_bucket:
|
||||
image: quay.io/minio/mc:latest
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
until mc alias set seaweedfs http://seaweedfs:8333 $S3_ACCESS_KEY_ID $S3_SECRET_ACCESS_KEY; do
|
||||
echo 'Waiting for SeaweedFS...';
|
||||
sleep 2;
|
||||
done;
|
||||
mc mb seaweedfs/$S3_BUCKET --ignore-existing;
|
||||
"
|
||||
networks:
|
||||
- reactive_resume_network
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
|
||||
reactive_resume:
|
||||
image: ghcr.io/amruthpillai/reactive-resume:latest
|
||||
networks:
|
||||
- traefik_network
|
||||
- reactive_resume_network
|
||||
volumes:
|
||||
- reactive_resume_data:/app/data
|
||||
environment:
|
||||
- APP_URL=$APP_URL
|
||||
# If using browserless with token auth, include the token in the URL:
|
||||
# PRINTER_ENDPOINT=ws://printer:3000?token=your-secret-token
|
||||
- PRINTER_ENDPOINT=$PRINTER_ENDPOINT
|
||||
- DATABASE_URL=$DATABASE_URL
|
||||
- AUTH_SECRET=$AUTH_SECRET
|
||||
- GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID
|
||||
- GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET
|
||||
- GITHUB_CLIENT_ID=$GITHUB_CLIENT_ID
|
||||
- GITHUB_CLIENT_SECRET=$GITHUB_CLIENT_SECRET
|
||||
- SMTP_HOST=$SMTP_HOST
|
||||
- SMTP_PORT=$SMTP_PORT
|
||||
- SMTP_USER=$SMTP_USER
|
||||
- SMTP_PASS=$SMTP_PASS
|
||||
- SMTP_FROM=$SMTP_FROM
|
||||
- SMTP_SECURE=$SMTP_SECURE
|
||||
- S3_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
|
||||
- S3_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
|
||||
- S3_REGION=$S3_REGION
|
||||
- S3_ENDPOINT=$S3_ENDPOINT
|
||||
- S3_BUCKET=$S3_BUCKET
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 1
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.app.rule=Host(`rxresu.me`)"
|
||||
- "traefik.http.routers.app.entrypoints=websecure"
|
||||
- "traefik.http.routers.app.tls=true"
|
||||
- "traefik.http.services.app.loadbalancer.server.port=3000"
|
||||
|
||||
configs:
|
||||
reactive_resume_postgres_config:
|
||||
name: reactive_resume_postgres_config
|
||||
external: true
|
||||
|
||||
networks:
|
||||
traefik_network:
|
||||
external: true
|
||||
reactive_resume_network:
|
||||
name: reactive_resume_network
|
||||
driver: overlay
|
||||
attachable: true
|
||||
|
||||
volumes:
|
||||
reactive_resume_postgres_data:
|
||||
name: reactive_resume_postgres_data
|
||||
reactive_resume_seaweedfs_data:
|
||||
name: reactive_resume_seaweedfs_data
|
||||
reactive_resume_data:
|
||||
name: reactive_resume_data
|
||||
```
|
||||
|
||||
**Deploy the stack:**
|
||||
|
||||
```bash
|
||||
docker stack deploy -c compose-swarm.yml reactive_resume
|
||||
```
|
||||
|
||||
**Useful commands:**
|
||||
|
||||
```bash
|
||||
# Check service status
|
||||
docker stack services reactive_resume
|
||||
|
||||
# View logs for the app
|
||||
docker service logs -f reactive_resume_app
|
||||
|
||||
# Scale the app
|
||||
docker service scale reactive_resume_app=3
|
||||
|
||||
# Remove the stack
|
||||
docker stack rm reactive_resume
|
||||
```
|
||||
|
||||
<Note>
|
||||
This example assumes you have an external Traefik network already set up. Adjust the `traefik_network` reference and labels based on your Traefik configuration.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## Contributing Your Setup
|
||||
|
||||
Have a different deployment setup that works well? Consider contributing it here. Some examples include:
|
||||
|
||||
- Kubernetes / Helm charts
|
||||
- Cloudflare Tunnel
|
||||
- Caddy reverse proxy
|
||||
- Docker with Portainer
|
||||
- Podman configurations
|
||||
- Cloud-specific deployments (AWS ECS, Google Cloud Run, Azure Container Apps)
|
||||
|
||||
To contribute, [open a pull request](https://github.com/amruthpillai/reactive-resume) with your example added to this page. Include:
|
||||
|
||||
1. A brief description of when/why someone would use this setup
|
||||
2. The complete Docker Compose (or equivalent) configuration
|
||||
3. Any additional configuration files (nginx.conf, etc.)
|
||||
4. Required environment variables
|
||||
@@ -0,0 +1,305 @@
|
||||
---
|
||||
title: "Migrating from v4 to v5"
|
||||
description: "A step-by-step guide to migrate your Reactive Resume instance from v4 to v5, including manual and automated migration options."
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This guide walks you through migrating your Reactive Resume installation from **v4 to v5**. The migration process involves setting up a new v5 instance alongside your existing v4 instance, then transferring your users and resumes to the new system.
|
||||
|
||||
<Warning>
|
||||
**Keep your v4 instance running** until you have successfully migrated all data to v5 and verified everything works correctly. This ensures you have a fallback in case anything goes wrong during the migration.
|
||||
</Warning>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting the migration, ensure you have:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Running v4 Instance">
|
||||
Your existing Reactive Resume v4 instance should be running and accessible.
|
||||
</Card>
|
||||
<Card title="New v5 Instance">
|
||||
A fresh Reactive Resume v5 instance set up and running. Follow the [Self-Hosting with Docker](/self-hosting/docker) guide if you haven't done this yet.
|
||||
</Card>
|
||||
<Card title="Database Access">
|
||||
Access to both your v4 PostgreSQL database (source) and v5 PostgreSQL database (target).
|
||||
</Card>
|
||||
<Card title="Backup">
|
||||
A recent backup of your v4 database. Always backup before any migration.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Choosing a Migration Method
|
||||
|
||||
The best migration approach depends on the size of your instance:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Manual Migration" icon="hand">
|
||||
**Best for**: Small instances with a handful of resumes.
|
||||
|
||||
Uses the built-in Import Dialog to manually convert resumes one at a time.
|
||||
</Card>
|
||||
<Card title="Automated Migration" icon="robot">
|
||||
**Best for**: Large instances with many users and resumes.
|
||||
|
||||
Uses migration scripts to batch-process all users and resumes automatically.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Manual Migration (Small Instances)
|
||||
|
||||
If you have only a few resumes to migrate, the simplest approach is to use the **Import Dialog** feature in v5.
|
||||
|
||||
<Steps>
|
||||
<Step title="Export from v4">
|
||||
In your v4 instance, go to each resume and export it as JSON. This creates a portable file containing all your resume data.
|
||||
</Step>
|
||||
|
||||
<Step title="Import into v5">
|
||||
In your new v5 instance:
|
||||
1. Log in or create a new account
|
||||
2. Click **Create Resume** or use the **Import** option
|
||||
3. Select the **Reactive Resume v4** format
|
||||
4. Upload your exported JSON file
|
||||
|
||||
The import process automatically converts the v4 format to v5.
|
||||
</Step>
|
||||
|
||||
<Step title="Verify and repeat">
|
||||
Review the imported resume to ensure all data transferred correctly. Repeat for each resume you need to migrate.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Tip>
|
||||
The Import Dialog handles the schema conversion automatically, so you don't need to worry about format differences between v4 and v5.
|
||||
</Tip>
|
||||
|
||||
## Automated Migration (Large Instances)
|
||||
|
||||
For instances with many users and resumes, use the migration scripts to automate the process. The migration happens in two phases: first users, then resumes.
|
||||
|
||||
### Requirements
|
||||
|
||||
To run the migration scripts, you need the following installed on your host machine:
|
||||
|
||||
<CardGroup cols={1}>
|
||||
<Card title="Node.js Runtime">
|
||||
**tsx** - TypeScript execution environment. Install globally with:
|
||||
```bash
|
||||
npm install -g tsx
|
||||
```
|
||||
</Card>
|
||||
<Card title="Environment Loader">
|
||||
**dotenvx** (or any tool to load `.env` files). Install globally with:
|
||||
```bash
|
||||
npm install -g @dotenvx/dotenvx
|
||||
```
|
||||
Alternatively, you can use `dotenv`, `direnv`, or export the variables manually.
|
||||
</Card>
|
||||
<Card title="Reactive Resume Source Code">
|
||||
Clone the Reactive Resume repository to access the migration scripts:
|
||||
```bash
|
||||
git clone https://github.com/amruthpillai/reactive-resume.git
|
||||
cd reactive-resume
|
||||
pnpm install
|
||||
```
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
### Environment Setup
|
||||
|
||||
Create a `.env` file in the root of the repository with the following variables:
|
||||
|
||||
```bash .env
|
||||
# Connection string to your NEW v5 PostgreSQL database (target)
|
||||
DATABASE_URL="postgresql://user:password@localhost:5432/reactive_resume_v5"
|
||||
|
||||
# Connection string to your OLD v4 PostgreSQL database (source)
|
||||
PRODUCTION_DATABASE_URL="postgresql://user:password@localhost:5432/reactive_resume_v4"
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Double-check your connection strings! `DATABASE_URL` should point to your **new v5 database** and `PRODUCTION_DATABASE_URL` should point to your **old v4 database**. Mixing these up could cause data loss.
|
||||
</Warning>
|
||||
|
||||
### Step 1: Migrate Users
|
||||
|
||||
The user migration script transfers all user accounts, authentication data, and two-factor settings from v4 to v5.
|
||||
|
||||
```bash
|
||||
dotenvx run -- tsx scripts/migration/user.ts
|
||||
```
|
||||
|
||||
**What this script does:**
|
||||
|
||||
- Fetches users in batches from the v4 database
|
||||
- Creates corresponding user accounts in the v5 database
|
||||
- Migrates authentication providers (email, Google, GitHub, custom OAuth)
|
||||
- Preserves two-factor authentication settings and backup codes
|
||||
- Creates a mapping file (`scripts/migration/user-id-map.json`) that links old user IDs to new ones
|
||||
|
||||
<Info>
|
||||
The script saves progress automatically. If interrupted (Ctrl+C), you can run it again and it will resume from where it left off.
|
||||
</Info>
|
||||
|
||||
**Expected output:**
|
||||
|
||||
```
|
||||
⌛ Starting user migration...
|
||||
📥 Fetching users batch from production database (OFFSET 0)...
|
||||
📋 Found 1000 users in this batch.
|
||||
📝 Preparing to bulk insert 1000 users...
|
||||
✅ Bulk inserted 1000 users in 245.3 ms (avg 0.2 ms/user)
|
||||
💾 Progress saved at offset 1000
|
||||
📦 Processed 1000 users so far...
|
||||
|
||||
📊 Migration Summary:
|
||||
Users created: 1000
|
||||
Accounts created: 1000
|
||||
Two-factor entries created: 50
|
||||
Skipped (already exist): 0
|
||||
⏱️ Total migration time: 1234.5 ms (1.23 seconds)
|
||||
✅ User migration complete!
|
||||
```
|
||||
|
||||
### Step 2: Migrate Resumes
|
||||
|
||||
After users are migrated, run the resume migration script. This script depends on the user ID mapping created in the previous step.
|
||||
|
||||
```bash
|
||||
dotenvx run -- tsx scripts/migration/resume.ts
|
||||
```
|
||||
|
||||
**What this script does:**
|
||||
|
||||
- Fetches resumes in batches from the v4 database
|
||||
- Converts each resume from v4 format to v5 format automatically
|
||||
- Links resumes to the correct users using the ID mapping
|
||||
- Migrates resume statistics (views, downloads)
|
||||
- Preserves visibility settings (public/private) and lock status
|
||||
|
||||
<Info>
|
||||
Like the user script, the resume migration also saves progress and can be resumed if interrupted.
|
||||
</Info>
|
||||
|
||||
**Expected output:**
|
||||
|
||||
```
|
||||
⌛ Starting resume migration...
|
||||
📥 Fetching resumes batch from production database (OFFSET 0)...
|
||||
📋 Found 2500 resumes in this batch.
|
||||
📝 Preparing to bulk insert 2500 resumes...
|
||||
✅ Bulk inserted 2500 resumes in 892.1 ms (avg 0.4 ms/resume)
|
||||
💾 Progress saved at offset 2500
|
||||
📦 Processed 2500 resumes so far...
|
||||
|
||||
📊 Migration Summary:
|
||||
Resumes created: 2500
|
||||
Statistics created: 2500
|
||||
Skipped (userId not found or already exist): 0
|
||||
Errors: 0
|
||||
⏱️ Total migration time: 5678.9 ms (5.68 seconds)
|
||||
✅ Resume migration complete!
|
||||
```
|
||||
|
||||
### Progress and Recovery
|
||||
|
||||
Both migration scripts support graceful shutdown and resume:
|
||||
|
||||
- **Progress files**: `scripts/migration/user-progress.json` and `scripts/migration/resume-progress.json` track the current migration state
|
||||
- **User ID mapping**: `scripts/migration/user-id-map.json` maps v4 user IDs to v5 user IDs
|
||||
- **Graceful shutdown**: Press `Ctrl+C` to stop the migration safely. Progress is saved before exit.
|
||||
- **Resume migration**: Run the script again to continue from where you left off
|
||||
|
||||
<Tip>
|
||||
If you need to restart the migration from scratch, delete the progress files and the user ID mapping file before running the scripts again.
|
||||
</Tip>
|
||||
|
||||
## Post-Migration Steps
|
||||
|
||||
After completing the migration:
|
||||
|
||||
<Steps>
|
||||
<Step title="Verify data integrity">
|
||||
Log into your v5 instance and spot-check several user accounts and resumes to ensure data transferred correctly.
|
||||
</Step>
|
||||
|
||||
<Step title="Test functionality">
|
||||
- Create a test resume and export it as PDF
|
||||
- Verify social logins work (if configured)
|
||||
- Check that two-factor authentication works for migrated users
|
||||
</Step>
|
||||
|
||||
<Step title="Update DNS/Proxy">
|
||||
Once verified, update your DNS records or reverse proxy to point to the new v5 instance.
|
||||
</Step>
|
||||
|
||||
<Step title="Decommission v4">
|
||||
After confirming everything works and allowing a grace period, you can safely shut down your v4 instance.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Important Notes
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="User passwords are preserved">
|
||||
Users who signed up with email/password can continue using their existing passwords. No password reset is required after migration.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Profile pictures are not migrated">
|
||||
User profile pictures (avatars) are stored as references in the database. If you were using S3 storage, ensure your v5 instance has access to the same bucket, or users may need to re-upload their avatars.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Resume images and uploads">
|
||||
Similar to profile pictures, any images embedded in resumes need to be accessible from your v5 instance. Consider migrating your storage bucket or updating references as needed.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="OAuth provider changes">
|
||||
If you're using custom OAuth providers, ensure the same providers are configured in v5 with matching client IDs. Users authenticate with the same provider ID, so mismatched configurations will cause login failures.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Schema differences">
|
||||
The v5 schema has some changes from v4:
|
||||
- `visibility` (public/private) is now `isPublic` (boolean)
|
||||
- Resume `title` is now `name`
|
||||
- Some resume data fields have been reorganized
|
||||
|
||||
The migration scripts handle these conversions automatically.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Script fails with 'PRODUCTION_DATABASE_URL is not set'">
|
||||
Ensure your `.env` file contains both `DATABASE_URL` and `PRODUCTION_DATABASE_URL`, and that you're using a tool like `dotenvx` to load them before running the script.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Users are skipped during migration">
|
||||
Users are skipped if:
|
||||
- Their email already exists in the v5 database
|
||||
- Their username already exists in the v5 database
|
||||
- They were already migrated in a previous run
|
||||
|
||||
Check the console output for skip reasons.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Resumes are skipped during migration">
|
||||
Resumes are skipped if:
|
||||
- The associated user wasn't migrated (user ID not in mapping file)
|
||||
- A resume with the same slug already exists for that user
|
||||
- They were already migrated in a previous run
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Resume data parsing fails">
|
||||
If a resume can't be parsed from v4 format, it will be created with default empty data. Check the console output for warnings about specific resumes, and consider manually importing those using the Import Dialog.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Migration is slow">
|
||||
The scripts process data in batches to avoid overwhelming the database. For very large instances:
|
||||
- Consider running the migration during off-peak hours
|
||||
- Ensure both databases have adequate resources
|
||||
- The batch size can be adjusted in the script files if needed
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -0,0 +1,321 @@
|
||||
---
|
||||
title: "Single Sign-On (SSO)"
|
||||
description: "A guide to setting up custom OAuth providers like Authentik, Authelia, Keycloak, or any OIDC-compliant identity provider for Single Sign-On (SSO) in your self-hosted instance."
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Reactive Resume supports custom OAuth providers, allowing you to integrate with enterprise identity providers and self-hosted authentication solutions. This is particularly useful for organizations that want to:
|
||||
|
||||
- Use a centralized identity provider (Authentik, Authelia, Keycloak, etc.)
|
||||
- Enforce Single Sign-On (SSO) across all internal applications
|
||||
- Integrate with existing LDAP/Active Directory infrastructure
|
||||
|
||||
<Info>
|
||||
Custom OAuth is designed for **self-hosted instances**. If you're using the hosted version at [rxresu.me](https://rxresu.me), you can use the built-in Google and GitHub sign-in options.
|
||||
</Info>
|
||||
|
||||
## Environment Variables
|
||||
|
||||
To enable a custom OAuth provider, you need to configure the following environment variables in your `.env` file:
|
||||
|
||||
### Required Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `OAUTH_CLIENT_ID` | The client ID provided by your OAuth provider |
|
||||
| `OAUTH_CLIENT_SECRET` | The client secret provided by your OAuth provider |
|
||||
|
||||
### Endpoint Configuration
|
||||
|
||||
You must configure endpoints using **one** of these two methods:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Option A: OIDC Discovery (Recommended)">
|
||||
For OIDC-compliant providers (most modern identity providers), you only need to set the discovery URL:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `OAUTH_DISCOVERY_URL` | Your provider's `.well-known/openid-configuration` URL |
|
||||
|
||||
The discovery URL automatically provides the authorization, token, and userinfo endpoints.
|
||||
|
||||
**Examples:**
|
||||
- Authentik: `https://auth.example.com/application/o/reactive-resume/.well-known/openid-configuration`
|
||||
- Keycloak: `https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration`
|
||||
- Authelia: `https://auth.example.com/.well-known/openid-configuration`
|
||||
</Tab>
|
||||
|
||||
<Tab title="Option B: Manual URLs">
|
||||
For providers that don't support OIDC discovery, you must set all three URLs:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `OAUTH_AUTHORIZATION_URL` | The URL where users are redirected to authorize |
|
||||
| `OAUTH_TOKEN_URL` | The URL to exchange authorization codes for tokens |
|
||||
| `OAUTH_USER_INFO_URL` | The URL to fetch user profile information |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Optional Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `OAUTH_PROVIDER_NAME` | Display name shown on the sign-in button | `Custom OAuth` |
|
||||
| `OAUTH_SCOPES` | Space-separated list of OAuth scopes | `openid profile email` |
|
||||
|
||||
## Callback URL
|
||||
|
||||
When configuring your OAuth provider, you'll need to set the **callback URL** (also called redirect URI). Use the following format:
|
||||
|
||||
```
|
||||
{APP_URL}/api/auth/oauth2/callback/custom
|
||||
```
|
||||
|
||||
For example, if your `APP_URL` is `https://resume.example.com`, the callback URL would be:
|
||||
|
||||
```
|
||||
https://resume.example.com/api/auth/oauth2/callback/custom
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Make sure the callback URL exactly matches what you configure in your OAuth provider. A mismatch will cause authentication to fail.
|
||||
</Warning>
|
||||
|
||||
## Profile Mapping
|
||||
|
||||
Reactive Resume automatically maps user profile data from the OAuth provider. The following fields are used:
|
||||
|
||||
| Reactive Resume Field | OAuth Profile Fields (in order of preference) |
|
||||
|-----------------------|-----------------------------------------------|
|
||||
| **Email** (required) | `email` |
|
||||
| **Name** | `name` → `preferred_username` → email prefix |
|
||||
| **Username** | `preferred_username` → email prefix |
|
||||
| **Avatar** | `image` → `picture` → `avatar_url` |
|
||||
|
||||
<Info>
|
||||
The OAuth provider **must** return an email address. If no email is provided, authentication will fail with an error.
|
||||
</Info>
|
||||
|
||||
## Provider-Specific Setup
|
||||
|
||||
### Authentik
|
||||
|
||||
<Steps>
|
||||
<Step title="Create an OAuth2/OpenID Provider">
|
||||
In the Authentik admin interface, navigate to **Applications → Providers** and create a new **OAuth2/OpenID Provider**.
|
||||
|
||||
- **Name**: Reactive Resume
|
||||
- **Authorization flow**: Use your preferred authorization flow
|
||||
- **Client type**: Confidential
|
||||
- **Redirect URIs**: `https://resume.example.com/api/auth/oauth2/callback/custom`
|
||||
</Step>
|
||||
|
||||
<Step title="Create an Application">
|
||||
Navigate to **Applications → Applications** and create a new application:
|
||||
|
||||
- **Name**: Reactive Resume
|
||||
- **Slug**: `reactive-resume`
|
||||
- **Provider**: Select the provider you just created
|
||||
</Step>
|
||||
|
||||
<Step title="Copy credentials">
|
||||
From the provider settings, copy the **Client ID** and **Client Secret**.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure environment variables">
|
||||
```bash .env
|
||||
OAUTH_PROVIDER_NAME="Authentik"
|
||||
OAUTH_CLIENT_ID="your-client-id"
|
||||
OAUTH_CLIENT_SECRET="your-client-secret"
|
||||
OAUTH_DISCOVERY_URL="https://auth.example.com/application/o/reactive-resume/.well-known/openid-configuration"
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Authelia
|
||||
|
||||
<Steps>
|
||||
<Step title="Configure an OIDC client">
|
||||
Add a client configuration to your Authelia `configuration.yml`:
|
||||
|
||||
```yaml
|
||||
identity_providers:
|
||||
oidc:
|
||||
clients:
|
||||
- client_id: reactive-resume
|
||||
client_name: Reactive Resume
|
||||
client_secret: 'your-hashed-secret' # Use authelia hash-password to generate
|
||||
public: false
|
||||
authorization_policy: two_factor # or one_factor
|
||||
redirect_uris:
|
||||
- https://resume.example.com/api/auth/oauth2/callback/custom
|
||||
scopes:
|
||||
- openid
|
||||
- profile
|
||||
- email
|
||||
token_endpoint_auth_method: client_secret_post
|
||||
```
|
||||
|
||||
<Info>
|
||||
Generate the hashed secret using: `authelia crypto hash generate pbkdf2 --variant sha512`
|
||||
</Info>
|
||||
</Step>
|
||||
|
||||
<Step title="Configure environment variables">
|
||||
```bash .env
|
||||
OAUTH_PROVIDER_NAME="Authelia"
|
||||
OAUTH_CLIENT_ID="reactive-resume"
|
||||
OAUTH_CLIENT_SECRET="your-plain-secret"
|
||||
OAUTH_DISCOVERY_URL="https://auth.example.com/.well-known/openid-configuration"
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Use the **plain text** secret in Reactive Resume's environment, not the hashed version used in Authelia's configuration.
|
||||
</Warning>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Keycloak
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a client">
|
||||
In the Keycloak admin console:
|
||||
|
||||
1. Select your realm
|
||||
2. Navigate to **Clients → Create client**
|
||||
3. Set **Client ID** (e.g., `reactive-resume`)
|
||||
4. Set **Client authentication** to **On**
|
||||
5. Enable **Standard flow**
|
||||
</Step>
|
||||
|
||||
<Step title="Configure redirect URI">
|
||||
In the client settings, add the redirect URI:
|
||||
|
||||
- **Valid redirect URIs**: `https://resume.example.com/api/auth/oauth2/callback/custom`
|
||||
</Step>
|
||||
|
||||
<Step title="Copy credentials">
|
||||
Go to the **Credentials** tab and copy the **Client secret**.
|
||||
</Step>
|
||||
|
||||
<Step title="Configure environment variables">
|
||||
```bash .env
|
||||
OAUTH_PROVIDER_NAME="Keycloak"
|
||||
OAUTH_CLIENT_ID="reactive-resume"
|
||||
OAUTH_CLIENT_SECRET="your-client-secret"
|
||||
OAUTH_DISCOVERY_URL="https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration"
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Generic OIDC Provider
|
||||
|
||||
For any other OIDC-compliant provider:
|
||||
|
||||
```bash .env
|
||||
OAUTH_PROVIDER_NAME="My SSO"
|
||||
OAUTH_CLIENT_ID="your-client-id"
|
||||
OAUTH_CLIENT_SECRET="your-client-secret"
|
||||
OAUTH_DISCOVERY_URL="https://sso.example.com/.well-known/openid-configuration"
|
||||
```
|
||||
|
||||
### Non-OIDC Provider (Manual Configuration)
|
||||
|
||||
For providers that don't support OIDC discovery:
|
||||
|
||||
```bash .env
|
||||
OAUTH_PROVIDER_NAME="Custom Provider"
|
||||
OAUTH_CLIENT_ID="your-client-id"
|
||||
OAUTH_CLIENT_SECRET="your-client-secret"
|
||||
OAUTH_AUTHORIZATION_URL="https://provider.example.com/oauth/authorize"
|
||||
OAUTH_TOKEN_URL="https://provider.example.com/oauth/token"
|
||||
OAUTH_USER_INFO_URL="https://provider.example.com/oauth/userinfo"
|
||||
OAUTH_SCOPES="openid profile email"
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a complete `.env` snippet showing custom OAuth alongside other authentication options:
|
||||
|
||||
```bash .env
|
||||
# --- Authentication ---
|
||||
AUTH_SECRET="your-32-byte-hex-secret"
|
||||
|
||||
# Built-in Social Auth (optional, can coexist with custom OAuth)
|
||||
# GOOGLE_CLIENT_ID=""
|
||||
# GOOGLE_CLIENT_SECRET=""
|
||||
# GITHUB_CLIENT_ID=""
|
||||
# GITHUB_CLIENT_SECRET=""
|
||||
|
||||
# Custom OAuth Provider (e.g., Authentik)
|
||||
OAUTH_PROVIDER_NAME="Company SSO"
|
||||
OAUTH_CLIENT_ID="reactive-resume-client-id"
|
||||
OAUTH_CLIENT_SECRET="reactive-resume-client-secret"
|
||||
OAUTH_DISCOVERY_URL="https://auth.company.com/application/o/reactive-resume/.well-known/openid-configuration"
|
||||
# OAUTH_SCOPES="openid profile email" # Defaults to these scopes if not set
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="'OAuth Provider did not return an email address' error">
|
||||
Your OAuth provider must return an email address for user creation. Ensure:
|
||||
- The `email` scope is included in your scopes
|
||||
- Your provider is configured to release the email claim
|
||||
- The user has an email address set in the identity provider
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Redirect URI mismatch error">
|
||||
The callback URL configured in your OAuth provider must exactly match:
|
||||
```
|
||||
{APP_URL}/api/auth/oauth2/callback/custom
|
||||
```
|
||||
Common issues:
|
||||
- Trailing slash mismatch
|
||||
- HTTP vs HTTPS mismatch
|
||||
- Port number differences
|
||||
- Path case sensitivity
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Custom OAuth button not appearing">
|
||||
The custom OAuth option only appears if both `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` are set, **and** either:
|
||||
- `OAUTH_DISCOVERY_URL` is set, **or**
|
||||
- All three manual URLs are set (`OAUTH_AUTHORIZATION_URL`, `OAUTH_TOKEN_URL`, `OAUTH_USER_INFO_URL`)
|
||||
|
||||
Double-check your environment variables and restart the container.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="CORS or network errors during authentication">
|
||||
If running behind a reverse proxy:
|
||||
- Ensure `APP_URL` matches your public URL
|
||||
- Verify the proxy passes the correct headers (`X-Forwarded-Proto`, `X-Forwarded-Host`)
|
||||
- Check that your OAuth provider allows the redirect URI from your domain
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="User profile data is missing or incorrect">
|
||||
The profile mapping depends on your provider returning standard claims:
|
||||
- `email` (required)
|
||||
- `name` or `preferred_username` for display name
|
||||
- `picture`, `image`, or `avatar_url` for avatar
|
||||
|
||||
Check your provider's documentation to ensure these claims are included in the ID token or userinfo response.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Security Considerations
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Use HTTPS" icon="lock">
|
||||
Always use HTTPS for both your Reactive Resume instance and OAuth provider in production. OAuth tokens should never be transmitted over unencrypted connections.
|
||||
</Card>
|
||||
<Card title="Protect secrets" icon="key">
|
||||
Never commit `OAUTH_CLIENT_SECRET` to version control. Use environment variables or a secrets manager.
|
||||
</Card>
|
||||
<Card title="Verify redirect URIs" icon="shield-check">
|
||||
Configure your OAuth provider to only allow the exact redirect URI. Avoid wildcards in redirect URI configurations.
|
||||
</Card>
|
||||
<Card title="Review scopes" icon="list-check">
|
||||
Only request the scopes you need. The default (`openid profile email`) is sufficient for Reactive Resume.
|
||||
</Card>
|
||||
</CardGroup>
|
||||