Compare commits

...
88 Commits
Author SHA1 Message Date
Amruth Pillai be9285aa33 chore(release): v5.1.2 2026-05-10 13:27:06 +02:00
Amruth Pillai 6787175a8a feat(ai): implement an AI chat window for agentic resume building (#3022) 2026-05-10 13:23:32 +02:00
Amruth Pillai 42e83cc676 fix: improper rendering of text blocks in PDFs 2026-05-10 13:22:21 +02:00
JamesGoslings 62f4532157 fix(pdf): register CJK fallback font so Chinese/Japanese/Korean text renders correctly (#3016)
Closes #2986.

Since v5.1.0 the renderer was migrated from Puppeteer to
@react-pdf/renderer. The new pipeline only registers the user-selected
typography family (e.g. Roboto, IBM Plex Serif), which contains no CJK
glyphs, so any Chinese / Japanese / Korean characters in the resume
fall back to .notdef and render as garbled boxes in both the in-app
preview and the exported PDF.

@react-pdf/renderer's textkit layer already supports per-codepoint
font substitution when a Text node is styled with `fontFamily` as a
string array — but only if every family in the stack has been
registered via Font.register. This change wires that up:

- packages/fonts: new `getPdfCjkFallbackFontFamily(family)` returns
  Noto Sans SC / Noto Serif SC depending on whether the primary font
  is sans-serif or serif, and `null` when no fallback is needed
  (standard PDF font, or primary already is the fallback). Source Han
  Sans/Serif SC covers all CJK-Unified ideographs, so a single font
  transparently handles Simplified/Traditional Chinese, Japanese
  kanji and Korean hanja.

- packages/pdf/hooks/use-register-fonts: after registering the
  primary body/heading fonts as before, additionally register the
  resolved CJK fallback (regular weight only — substitution is
  per-codepoint, not per-weight, so one face is enough). The
  function's return type is widened to a new `PdfTypography` whose
  `body.fontFamily` and `heading.fontFamily` become
  `[primary, cjkFallback]` two-element stacks.

- packages/pdf/document: cast the widened typography back through the
  schema-typed `ResumeData` so the wider runtime value reaches
  templates without changing the public `Typography` schema. All 15
  templates already consume `metadata.typography.body.fontFamily`
  directly, and `StyleSheet.fontFamily` accepts both string and
  string[], so no template edits are required.

Latin-only resumes are unaffected:
- `getPdfCjkFallbackFontFamily` returns `null` for standard PDF fonts
  and existing CJK selections, so the extra Font.register call is
  skipped.
- When no fallback applies, `registerFonts` returns the original
  typography reference unchanged (zero allocation).
- Even when the fallback is registered, textkit only consults it for
  codepoints the primary font cannot render, so Latin glyphs still
  come from the user-selected font with identical metrics.
2026-05-09 18:49:59 +02:00
Donovan WattsandAmruth Pillai fabe22089d fix(api): allow empty name in public resume output schema (#3012)
The redactResumeForViewer helper intentionally blanks the dashboard
title for non-owner viewers (the title can leak owner-only context like
"Senior Eng @ Foo - final draft"), but the getBySlug output schema
inherits name.min(1) from resumeSchema. Zod rejects the redacted
payload, oRPC throws Output validation failed, and every public resume
URL returns HTTP 500.

Relax the constraint to z.string() on the getBySlug output only -
ownership-gated procedures (getById, update, patch, list, ...) keep
min(1).

Fixes #3011

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-05-09 18:49:43 +02:00
Nicolas Oddo fa38f3e84a fix: correct v4 to v5 import for custom sections, skill levels, and hidden items (#3013) 2026-05-09 18:48:21 +02:00
github-actions[bot]andCrowdin Bot 0606e0072b [skip ci] chore(i18n): sync translations from crowdin (#3005)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-08 16:19:27 +02:00
Amruth PillaiandClaude bdfb854602 fix: resolve storage healthcheck path via LOCAL_STORAGE_PATH env var (#3004)
* fix: resolve local data directory to /app/data in production Docker

In the official Docker image, cwd is /app/apps/web (set via WORKDIR), but
the data volume is mounted at /app/data. Without pnpm-workspace.yaml present
in the runtime image, findWorkspaceRoot() returns null, so getLocalDataDirectory()
fell back to <cwd>/data = /app/apps/web/data, which the node user has no
permission to create. This caused the storage healthcheck to fail with
EACCES.

Add a production fallback: when cwd ends in apps/web, resolve the data
directory to two levels up (matching /app/data in the official image).

Re-resolves #2990.

https://claude.ai/code/session_015pSTtukxf7mFTty2Y6PHZf

* fix: replace apps/web heuristic with LOCAL_STORAGE_PATH env var

The previous fix special-cased a cwd ending in apps/web to land on /app/data,
but the heuristic could false-positive on any path with that suffix and was
fragile to Dockerfile changes. pnpm-workspace.yaml is never copied into the
runtime image, so the workspace-root walk was also dead code in production.

Replace the heuristic with an explicit LOCAL_STORAGE_PATH env var:
- Set LOCAL_STORAGE_PATH=/app/data in the Dockerfile (single source of truth).
- Add LOCAL_STORAGE_PATH to the env schema; storage and statistics services
  pass it through to getLocalDataDirectory.
- getLocalDataDirectory now uses the override when set, else workspace root
  (dev), else cwd/data.
- New Nitro plugin validates the resolved local data directory at startup
  and refuses to boot with a clear error if it isn't writable, surfacing
  permission issues immediately instead of at first upload/healthcheck.
- Document the new variable in .env.example and the Docker self-hosting docs.

https://claude.ai/code/session_015pSTtukxf7mFTty2Y6PHZf

* fix: address review feedback on storage path handling

- apps/web/plugins/2.storage.ts: use the default-import style for
  node:fs/promises (matches the rest of the repo, sidesteps any
  named-export concerns for fs.constants).
- packages/env/src/server.ts: reject relative LOCAL_STORAGE_PATH values
  via a zod refinement. Relative paths would be resolved against cwd,
  which differs between dev and Docker — exactly the same surprise the
  original bug had. Failing fast at config validation time gives a
  clear error before the server boots.

https://claude.ai/code/session_015pSTtukxf7mFTty2Y6PHZf

* fix: update data volume configuration in Docker Compose and enhance Nitro plugin

* fix: remove "Can I customize the templates?" FAQ entry from multiple language files

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-08 16:05:47 +02:00
github-actions[bot]andCrowdin Bot 05f094bd13 [skip ci] chore(i18n): sync translations from crowdin (#3001)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-08 13:07:45 +02:00
github-actions[bot]andCrowdin Bot d1c301de83 [skip ci] chore(i18n): sync translations from crowdin (#2999)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-08 13:06:37 +02:00
Amruth PillaiandClaude e42af3cd04 Remove "Can I customize the templates?" FAQ from homepage (#3000)
https://claude.ai/code/session_013T4jd8fPiWR5jMw2yxwgez

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-08 13:06:27 +02:00
Amruth Pillai f3375adecb chore(release): v5.1.1 2026-05-08 13:05:55 +02:00
Amruth Pillai d9e3289f69 chore: remove unused dependencies and update locale references in resume components 2026-05-08 13:01:18 +02:00
github-actions[bot]andCrowdin Bot 7c08007a9d [skip ci] chore(i18n): sync translations from crowdin (#2997)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-08 12:39:10 +02:00
Amruth Pillai 296f7951ec fix: make nested lists work in PDF renderer, resolves #2993 2026-05-08 12:29:16 +02:00
github-actions[bot]andCrowdin Bot bf1a540fd1 [skip ci] chore(i18n): sync translations from crowdin (#2995)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-08 11:58:47 +02:00
Amruth Pillai e4a574ccd0 chore: remove tests 2026-05-08 11:58:25 +02:00
Amruth Pillai 26ca5c29c3 fix: auto-load .env, resolves #2987 2026-05-08 11:57:59 +02:00
Amruth Pillai 2cd774dab7 feat: implement free-form resume page formats, resolves #2991 2026-05-08 11:28:18 +02:00
Amruth Pillai 9cbb30d3ba fix: resolves #2990, revert the local storage path to /app/data 2026-05-08 11:10:24 +02:00
Amruth Pillai ed42f181ca fix: update dialog component styles for improved responsiveness and overflow handling 2026-05-08 10:39:34 +02:00
Amruth Pillai 21cadd76fe fix: update sample resume picture URL to point to a valid image 2026-05-08 02:37:01 +02:00
Amruth Pillai 4c771307e0 fix: fallback to "IBM Plex Serif" when unknown font encountered 2026-05-08 00:51:07 +02:00
Amruth Pillai 6d54ffa88b fix: font registration to support italic styles in PDF generation 2026-05-08 00:29:10 +02:00
Amruth Pillai 5042ad9d1f feat: add PDF download endpoint and export createLocalizedResumeDocument function 2026-05-08 00:05:55 +02:00
Amruth Pillai 3b82aa90f3 fix: resolves #2793, accept looseObject from resumeDataSchema 2026-05-07 22:50:45 +02:00
Amruth Pillai 524a7a6f4e fix: resolves #2949, remove route preloading 2026-05-07 22:46:32 +02:00
Amruth Pillai 36a16f6483 fix(#2983): fixes #2983, experience role progression causes re-renders 2026-05-07 22:30:47 +02:00
Amruth Pillai e320f3a920 chore: remove @tanstack/zod-adapter 2026-05-07 22:22:58 +02:00
Amruth Pillai d5891ff035 fix(#2978): implement inlineLink functionality 2026-05-07 21:47:00 +02:00
Amruth Pillai 891ce60270 fix(#2984): add prose styles to rich-input editor 2026-05-07 21:43:07 +02:00
Amruth Pillai 1b0bb067b6 fix(#2976): update header name line height across multiple templates 2026-05-07 21:34:01 +02:00
autofix-ci[bot] 2b3af1f7b7 [autofix.ci] apply automated fixes 2026-05-07 19:29:59 +00:00
Amruth Pillai ced765e229 fix(#2978): show underline on all links 2026-05-07 21:29:10 +02:00
Amruth Pillai 023cb4a594 fix(#2981): runtime error when enabling 2fa 2026-05-07 21:23:53 +02:00
github-actions[bot]andCrowdin Bot a43538c919 [skip ci] chore(i18n): sync translations from crowdin (#2975)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-07 16:52:18 +02:00
Amruth Pillai faba604813 ci: do not run lingui:extract on autofix 2026-05-07 16:51:59 +02:00
github-actions[bot]andCrowdin Bot f8b3437e33 [skip ci] chore(i18n): sync translations from crowdin (#2974)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-07 16:50:29 +02:00
Amruth Pillai 4ac16df3d6 Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-05-07 16:49:48 +02:00
Amruth Pillai 4ae3e1a230 fix: update resume ID type from uuid to text and use generateId for ID generation 2026-05-07 16:49:44 +02:00
autofix-ci[bot] dcccbcfa9e [autofix.ci] apply automated fixes 2026-05-07 14:49:24 +00:00
Amruth Pillai 92a3916e76 fix: convert postgres uuid to text columns, to capture old cuid resume IDs 2026-05-07 16:48:25 +02:00
github-actions[bot]andCrowdin Bot 6321e99b9e [skip ci] chore(i18n): sync translations from crowdin (#2973)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-07 16:38:34 +02:00
github-actions[bot]andCrowdin Bot 94c953005e [skip ci] chore(i18n): sync translations from crowdin (#2972)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-07 16:31:49 +02:00
Amruth Pillai cf957692fc chore: update phosphor-icons-react-pdf to version 0.1.3 in package.json and pnpm-lock.yaml 2026-05-07 16:31:26 +02:00
Amruth Pillai 64492ce2f1 Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-05-07 15:19:19 +02:00
Amruth Pillai 4165c10a71 fix: readme banner image url 2026-05-07 15:19:13 +02:00
autofix-ci[bot] ef6961c9eb [autofix.ci] apply automated fixes 2026-05-07 13:18:55 +00:00
Amruth Pillai 13787db34a Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-05-07 15:16:34 +02:00
github-actions[bot]andCrowdin Bot f64daeb89e [skip ci] chore(i18n): sync translations from crowdin (#2971)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-07 15:15:23 +02:00
Amruth Pillai 6e447d1bcc Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-05-07 15:14:21 +02:00
Amruth Pillai 148d564fe8 fix: readme banner image url 2026-05-07 15:14:20 +02:00
Amruth Pillai 50ba37a27f v5.1.0 (#2970)
* chore(release): v5.1.0

* feat: implement resume thumbnails

* fix: remove unused mcp tools

* docs: fix formatting of docs
2026-05-07 15:12:33 +02:00
Joshua Fife 51c366310e Fixed docker compose for linux self hosted deployments (#2952) 2026-05-02 23:57:27 +02:00
Amruth Pillai 4e58ea6c8a chore: remove .devcontainer 2026-04-30 02:07:58 +02:00
Amruth Pillai fd892b0e14 fix: make the dash plugin optional
- fixes #2947
2026-04-29 23:30:07 +02:00
Amruth Pillai dc4830d41b chore: remove getSubsequentPageTopMarginStyle 2026-04-29 21:42:20 +02:00
Amruth Pillai 480e9a2612 fix: switch back to csp-report-only 2026-04-29 21:40:57 +02:00
Amruth Pillai 4cf5cdd181 fix: remove timeout to check data-wf-loaded 2026-04-29 21:38:40 +02:00
Amruth Pillai b9894c687c fix: handle /assets/ requests before it reaches orpc 2026-04-29 20:40:28 +02:00
autofix-ci[bot] 9048702cb8 [autofix.ci] apply automated fixes 2026-04-29 18:17:46 +00:00
Kuchizu dd94d070af fix: make AI resume analysis schema provider-compatible (#2939) 2026-04-29 20:16:29 +02:00
Amruth Pillai e035088269 fix: add-to-cache-list-conflicting-entries 2026-04-29 20:15:15 +02:00
Amruth Pillai 1fbe89bc01 update ipAddressHeaders 2026-04-29 19:55:31 +02:00
Amruth Pillai 4b071f2db7 update ipAddressHeaders 2026-04-29 19:54:21 +02:00
Amruth Pillai 80ca3a8875 update Dockerfile 2026-04-29 19:34:41 +02:00
Amruth Pillai e95be536fa remove CSP reporting 2026-04-29 19:19:36 +02:00
Amruth Pillai d6287dbd65 better rate limiting, verbose logging for username/slug path 2026-04-29 18:44:07 +02:00
Amruth Pillai 47abce351b sync translations with crowdin 2026-04-27 15:22:13 +02:00
Amruth Pillai 8f6c65b7fd chore(release): 🚀 v5.0.20 2026-04-27 15:21:07 +02:00
github-actions[bot]andCrowdin Bot 5a88ac12bf Sync Translations from Crowdin (#2935)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-27 15:13:05 +02:00
RuzenieAmruth Pillaiautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
118004b3d3 feat:icon colors (#2928)
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-27 15:12:55 +02:00
github-actions[bot]andCrowdin Bot 6a8dd480cb Sync Translations from Crowdin (#2934)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-27 13:39:40 +02:00
Amruth Pillai 5d8126d4b0 feat: combine settings pages into a single integrations page 2026-04-27 12:56:15 +02:00
JamesGoslings 8566a9fd0f fix(i18n): translate Skills section title to Chinese (zh-CN) (#2931) 2026-04-27 10:47:46 +02:00
github-actions[bot]andCrowdin Bot 0d0bc61582 Sync Translations from Crowdin (#2932)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-27 10:47:22 +02:00
JamesGoslings e3af637100 docs: list Meowth alongside existing templates (#2929) 2026-04-27 10:47:14 +02:00
github-actions[bot]andCrowdin Bot f752145c57 Sync Translations from Crowdin (#2926)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-27 10:45:59 +02:00
Amruth Pillai b87f200767 feat: Add better email templates for password reset and email verification. 2026-04-27 10:45:44 +02:00
Amruth Pillai 73ec8b2ffb chore: release version 5.0.20 with new Meowth resume template and updated translations 2026-04-27 08:52:18 +02:00
github-actions[bot]andCrowdin Bot 107537999f Sync Translations from Crowdin (#2925)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-27 08:50:43 +02:00
Amruth Pillai e1bccbcc93 chore: update dependencies, cleanup exports (knip) 2026-04-27 08:48:35 +02:00
JamesGoslingsAmruth Pillaiautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
54e256be09 feat: add Meowth template with inline three-column entry header (Asian-style compact ATS) (#2923)
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-27 08:45:08 +02:00
github-actions[bot]andCrowdin Bot b26d510294 Sync Translations from Crowdin (#2922)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-04-26 11:46:56 +02:00
Amruth Pillai 623ca5c675 allow loopback to localhost on mcp, only for local MCP clients 2026-04-26 10:59:10 +02:00
Amruth PillaiandClaude 76e00ae019 Revert "fix(resume): prevent orphaned section headings at page break (#2851)" (#2920)
This reverts commit 775f625f6e.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-26 01:23:22 +02:00
Amruth Pillai ff0ef12ecc Merge branch 'main' of github.com:amruthpillai/reactive-resume 2026-04-26 01:08:00 +02:00
Amruth Pillai 1bcb2e2af6 update changelog 2026-04-26 01:07:58 +02:00
1034 changed files with 125918 additions and 143172 deletions
-10
View File
@@ -1,10 +0,0 @@
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
-33
View File
@@ -1,33 +0,0 @@
{
"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": [
"oxc.oxc-vscode",
"bradlc.vscode-tailwindcss",
"lokalise.i18n-ally",
"typescriptteam.native-preview"
],
"settings": {
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.oxc": "always"
}
}
}
},
"postCreateCommand": "corepack enable && pnpm install"
}
-95
View File
@@ -1,95 +0,0 @@
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:
+23 -7
View File
@@ -1,15 +1,31 @@
dist
docs
# VCS and editor state
.git
.gitignore
.cursor
.DS_Store
# Local configuration and runtime state
.env*
/data
# Dependency and package-manager caches
node_modules
**/node_modules
.pnpm-store
# Build, test, and framework output
dist
**/dist
.nitro
.cursor
.output
.vercel
.netlify
coverage
.DS_Store
.tanstack
node_modules
.turbo
**/.turbo
coverage
reports
# Generated service-worker assets are rebuilt during the web build.
public/sw.js
public/workbox-*.js
public/workbox-*.js
+35 -27
View File
@@ -1,18 +1,14 @@
# --- Server ---
TZ="Etc/UTC"
# --- Application ---
# Port used by the web server in local development and self-hosted containers.
PORT="3000"
# Public URL where the app is served. Used for auth callbacks, OAuth issuer URLs,
# OpenGraph metadata, and absolute upload URLs.
APP_URL="http://localhost:3000"
# Optional, uses APP_URL by default
# 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"
# --- Printer ---
PRINTER_ENDPOINT="ws://localhost:4000?token=change-me"
# --- Database (PostgreSQL) ---
# PostgreSQL connection URL. In Docker Compose, the hostname is usually `postgres`;
# when running directly on your machine, `localhost` is typical.
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
# --- Authentication ---
@@ -20,34 +16,45 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
AUTH_SECRET="change-me-to-a-secure-secret-key-in-production"
# Better Auth Dashboard (optional)
# Enables the Better Auth Dashboard plugin when set, you probably don't need this.
BETTER_AUTH_API_KEY=""
# Social Auth (Google, optional)
# Set both values to enable Google sign-in.
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# Social Auth (GitHub, optional)
# Set both values to enable GitHub sign-in.
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# Social Auth (LinkedIn, optional)
# Set both values to enable LinkedIn sign-in.
LINKEDIN_CLIENT_ID=""
LINKEDIN_CLIENT_SECRET=""
# Custom OAuth Provider (optional)
# Set OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET plus either OAUTH_DISCOVERY_URL or
# the three manual endpoint URLs below.
OAUTH_PROVIDER_NAME=""
OAUTH_CLIENT_ID=""
OAUTH_CLIENT_SECRET=""
OAUTH_DISCOVERY_URL=""
OAUTH_AUTHORIZATION_URL=""
OAUTH_TOKEN_URL=""
OAUTH_USER_INFO_URL=""
# Space-separated scopes requested from the custom OAuth provider.
OAUTH_SCOPES="openid profile email"
# Comma-separated extra hosts/origins allowed for dynamic OAuth client redirect URIs.
# By default, only the APP_URL origin is allowed.
OAUTH_DYNAMIC_CLIENT_REDIRECT_HOSTS=""
# AI provider base URL allowlist (optional, comma-separated hosts/origins)
# Example: api.openai.com,https://gateway.ai.vercel.com
AI_ALLOWED_BASE_URLS=""
# --- Email (optional) ---
# If all keys are disabled, the app logs the email to be sent to the console instead.
# If SMTP_HOST, SMTP_USER, SMTP_PASS, or SMTP_FROM is missing, the app logs the
# email to the console instead.
SMTP_HOST="localhost"
SMTP_PORT="1025"
SMTP_USER=""
@@ -56,8 +63,11 @@ SMTP_FROM="Reactive Resume <noreply@rxresu.me>"
SMTP_SECURE="false"
# --- Storage (optional) ---
# If all keys are disabled, the app uses local filesystem (/data) to store uploads instead.
# If all S3 keys are disabled, the app uses local filesystem storage instead.
# Make sure to mount this directory to a volume or the host filesystem to ensure data integrity.
# LOCAL_STORAGE_PATH overrides where local uploads/cache are written.
# Defaults to /app/data in the official Docker image; in dev, defaults to <workspace>/data.
# LOCAL_STORAGE_PATH="/app/data"
# Seaweedfs
S3_ACCESS_KEY_ID="seaweedfs"
@@ -68,14 +78,11 @@ S3_BUCKET="reactive-resume"
S3_FORCE_PATH_STYLE="true"
# --- Feature Flags ---
# This flag bypasses the check that the endpoint to fetch data for the printing of PDFs `getByIdForPrinter`, is only accessible from the server.
# Useful for when you want to debug the /printer/{resumeId} route to quickly take a peek at the page that is sent to the printer.
FLAG_DEBUG_PRINTER="false"
# This flag disables new signups, both on the web app and the server.
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.
# 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.
@@ -84,10 +91,11 @@ FLAG_DISABLE_IMAGE_PROCESSING="false"
# --- Others ---
# Google Cloud API Key (optional)
# This is not used within Reactive Resume, but in src/scripts/fonts/generate.ts to generate a list of fonts served by Google Fonts.
# Note: Make sure "Google Fonts Developer API" is unrestricted.
# For font-list generation tooling.
# Requires "Google Fonts Developer API" to be enabled.
GOOGLE_CLOUD_API_KEY=""
# --- Crowdin ---
# Crowdin (optional)
# For translation tooling.
CROWDIN_PROJECT_ID=""
CROWDIN_API_TOKEN=""
CROWDIN_API_TOKEN=""
-1
View File
@@ -1 +0,0 @@
locales/*.po linguist-generated=true
+66 -66
View File
@@ -1,68 +1,68 @@
{
"$schema": "https://fundingjson.org/schema/v1.1.0.json",
"version": "v1.1.0",
"entity": {
"type": "individual",
"role": "maintainer",
"name": "Amruth Pillai",
"email": "im.amruth@gmail.com",
"description": "Software Engineer",
"webpageUrl": {
"url": "https://rxresu.me/funding.json"
}
},
"projects": [
{
"guid": "reactive-resume",
"name": "Reactive Resume",
"description": "A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.",
"webpageUrl": {
"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"
},
"licenses": ["spdx:MIT"],
"tags": ["data", "design", "productivity", "resume-builder"]
}
],
"funding": {
"plans": [
{
"guid": "sponsor",
"status": "active",
"name": "Sponsor",
"description": "Support the project on a recurring basis by becoming a sponsor.",
"amount": 10,
"currency": "EUR",
"frequency": "monthly",
"channels": ["github", "open-collective"]
},
{
"guid": "donation",
"status": "active",
"name": "Donation",
"description": "Show your support for the project by making a one-time donation.",
"amount": 0,
"currency": "EUR",
"frequency": "one-time",
"channels": ["github", "open-collective"]
}
],
"channels": [
{
"guid": "github",
"type": "payment-provider",
"description": "GitHub Sponsors",
"address": "https://github.com/sponsors/AmruthPillai"
},
{
"guid": "open-collective",
"type": "payment-provider",
"description": "Open Collective",
"address": "https://opencollective.com/reactive-resume"
}
]
}
"$schema": "https://fundingjson.org/schema/v1.1.0.json",
"version": "v1.1.0",
"entity": {
"type": "individual",
"role": "maintainer",
"name": "Amruth Pillai",
"email": "hello@amruthpillai.com",
"description": "Software Engineer",
"webpageUrl": {
"url": "https://rxresu.me/funding.json"
}
},
"projects": [
{
"guid": "reactive-resume",
"name": "Reactive Resume",
"description": "A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.",
"webpageUrl": {
"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"
},
"licenses": ["spdx:MIT"],
"tags": ["data", "design", "productivity", "resume-builder"]
}
],
"funding": {
"plans": [
{
"guid": "sponsor",
"status": "active",
"name": "Sponsor",
"description": "Support the project on a recurring basis by becoming a sponsor.",
"amount": 10,
"currency": "EUR",
"frequency": "monthly",
"channels": ["github", "open-collective"]
},
{
"guid": "donation",
"status": "active",
"name": "Donation",
"description": "Show your support for the project by making a one-time donation.",
"amount": 0,
"currency": "EUR",
"frequency": "one-time",
"channels": ["github", "open-collective"]
}
],
"channels": [
{
"guid": "github",
"type": "payment-provider",
"description": "GitHub Sponsors",
"address": "https://github.com/sponsors/AmruthPillai"
},
{
"guid": "open-collective",
"type": "payment-provider",
"description": "Open Collective",
"address": "https://opencollective.com/reactive-resume"
}
]
}
}
+12 -9
View File
@@ -19,20 +19,23 @@ jobs:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Setup Vite+
uses: voidzero-dev/setup-vp@v1
- name: Install pnpm
uses: pnpm/action-setup@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "24"
cache: true
node-version: "lts/*"
cache: "pnpm"
- name: Install Dependencies
run: vp install --frozen-lockfile
run: pnpm install --frozen-lockfile
- name: Run Tests
run: vp test
- name: Remove Unused Dependencies
run: pnpm knip --fix
- name: Run Lint and Format
run: vp check --fix
- name: Lint and Format
run: pnpm check
- name: Autofix
uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8
+51 -18
View File
@@ -1,21 +1,54 @@
dist
.env*
/data
.nitro
.agents
# Dependencies
node_modules
.pnpm-store
# Build Outputs
.output
.vercel
.cursor
.claude
TODO.md
coverage
.netlify
.DS_Store
.tanstack
node_modules
.wrangler
# Environment Variables
.env*
!.env.example
public/sw.js
public/sw.js.map
scripts/**/*.json
public/workbox-*.js
public/workbox-*.js.map
# IDEs and Editors
*~
*.swp
*.swo
.DS_Store
.idea
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Log Files
logs
*.log*
# Turborepo
.turbo
# TanStack Start
.tanstack
# Testing
coverage
reports
# Cache
tmp
temp
.cache
# AI
.codex
.claude
.cursor
.superpowers
docs/superpowers
# Local Storage Data
/data
/apps/web/data
+8 -10
View File
@@ -1,17 +1,15 @@
// @ts-check
const nextPackages = ["@monaco-editor/react"];
const betaPackages = ["drizzle-orm", "drizzle-kit", "drizzle-zod"];
/** @type {import('npm-check-updates').RunOptions} */
module.exports = {
upgrade: true,
install: "always",
packageManager: "pnpm",
target: (packageName) => {
if (nextPackages.includes(packageName)) return "@next";
if (betaPackages.includes(packageName)) return "@beta";
return "latest";
},
upgrade: true,
workspaces: true,
install: "always",
packageManager: "pnpm",
target: (packageName) => {
if (betaPackages.includes(packageName)) return "@beta";
return "latest";
},
};
+71
View File
@@ -0,0 +1,71 @@
#!/bin/sh
if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then
set -x
fi
if [ "$LEFTHOOK" = "0" ]; then
exit 0
fi
call_lefthook()
{
if test -n "$LEFTHOOK_BIN"
then
"$LEFTHOOK_BIN" "$@"
elif lefthook -h >/dev/null 2>&1
then
lefthook "$@"
elif /Users/amruth/Projects/reactive-resume/node_modules/.pnpm/lefthook-darwin-arm64@2.1.6/node_modules/lefthook-darwin-arm64/bin/lefthook -h >/dev/null 2>&1
then
/Users/amruth/Projects/reactive-resume/node_modules/.pnpm/lefthook-darwin-arm64@2.1.6/node_modules/lefthook-darwin-arm64/bin/lefthook "$@"
else
dir="$(git rev-parse --show-toplevel)"
osArch=$(uname | tr '[:upper:]' '[:lower:]')
cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/')
if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook"
then
"$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook"
then
"$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook"
then
"$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@"
elif test -f "$dir/node_modules/lefthook/bin/index.js"
then
"$dir/node_modules/lefthook/bin/index.js" "$@"
elif go tool lefthook -h >/dev/null 2>&1
then
go tool lefthook "$@"
elif bundle exec lefthook -h >/dev/null 2>&1
then
bundle exec lefthook "$@"
elif yarn lefthook -h >/dev/null 2>&1
then
yarn lefthook "$@"
elif pnpm lefthook -h >/dev/null 2>&1
then
pnpm lefthook "$@"
elif swift package lefthook >/dev/null 2>&1
then
swift package --build-path .build/lefthook --disable-sandbox lefthook "$@"
elif command -v mint >/dev/null 2>&1
then
mint run csjones/lefthook-plugin "$@"
elif uv run lefthook -h >/dev/null 2>&1
then
uv run lefthook "$@"
elif mise exec -- lefthook -h >/dev/null 2>&1
then
mise exec -- lefthook "$@"
elif devbox run lefthook -h >/dev/null 2>&1
then
devbox run lefthook "$@"
else
echo "Can't find lefthook in PATH"
fi
fi
}
call_lefthook run "commit-msg" "$@"
+71
View File
@@ -0,0 +1,71 @@
#!/bin/sh
if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then
set -x
fi
if [ "$LEFTHOOK" = "0" ]; then
exit 0
fi
call_lefthook()
{
if test -n "$LEFTHOOK_BIN"
then
"$LEFTHOOK_BIN" "$@"
elif lefthook -h >/dev/null 2>&1
then
lefthook "$@"
elif /Users/amruth/Projects/reactive-resume/node_modules/.pnpm/lefthook-darwin-arm64@2.1.6/node_modules/lefthook-darwin-arm64/bin/lefthook -h >/dev/null 2>&1
then
/Users/amruth/Projects/reactive-resume/node_modules/.pnpm/lefthook-darwin-arm64@2.1.6/node_modules/lefthook-darwin-arm64/bin/lefthook "$@"
else
dir="$(git rev-parse --show-toplevel)"
osArch=$(uname | tr '[:upper:]' '[:lower:]')
cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/')
if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook"
then
"$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook"
then
"$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@"
elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook"
then
"$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@"
elif test -f "$dir/node_modules/lefthook/bin/index.js"
then
"$dir/node_modules/lefthook/bin/index.js" "$@"
elif go tool lefthook -h >/dev/null 2>&1
then
go tool lefthook "$@"
elif bundle exec lefthook -h >/dev/null 2>&1
then
bundle exec lefthook "$@"
elif yarn lefthook -h >/dev/null 2>&1
then
yarn lefthook "$@"
elif pnpm lefthook -h >/dev/null 2>&1
then
pnpm lefthook "$@"
elif swift package lefthook >/dev/null 2>&1
then
swift package --build-path .build/lefthook --disable-sandbox lefthook "$@"
elif command -v mint >/dev/null 2>&1
then
mint run csjones/lefthook-plugin "$@"
elif uv run lefthook -h >/dev/null 2>&1
then
uv run lefthook "$@"
elif mise exec -- lefthook -h >/dev/null 2>&1
then
mise exec -- lefthook "$@"
elif devbox run lefthook -h >/dev/null 2>&1
then
devbox run lefthook "$@"
else
echo "Can't find lefthook in PATH"
fi
fi
}
call_lefthook run "pre-commit" "$@"
-1
View File
@@ -1 +0,0 @@
vp staged
+1 -7
View File
@@ -1,9 +1,3 @@
{
"recommendations": [
"oxc.oxc-vscode",
"bradlc.vscode-tailwindcss",
"lokalise.i18n-ally",
"typescriptteam.native-preview",
"VoidZero.vite-plus-extension-pack"
]
"recommendations": ["biomejs.biome", "bradlc.vscode-tailwindcss", "typescriptteam.native-preview"]
}
+29 -39
View File
@@ -1,41 +1,31 @@
{
"editor.defaultFormatter": "oxc.oxc-vscode",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.fixAll.oxc": "explicit"
},
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"files.associations": {
"*.css": "tailwindcss"
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true,
"pnpm-lock.yaml": true
},
"files.watcherExclude": {
"**/routeTree.gen.ts": true,
"locales/**.po": true,
"pnpm-lock.yaml": true
},
"i18n-ally.enabledParsers": ["po"],
"i18n-ally.localesPaths": ["locales"],
"i18n-ally.sourceLanguage": "en-US",
"oxc.enable.oxfmt": true,
"oxc.enable.oxlint": true,
"oxc.configPath": "./vite.config.ts",
"oxc.fmt.configPath": "./vite.config.ts",
"search.exclude": {
"**/routeTree.gen.ts": true,
"locales/**.po": true,
"pnpm-lock.yaml": true
},
"tailwindCSS.classFunctions": ["cn", "cva"],
"tailwindCSS.experimental.classRegex": [
["class:\\s*?[\"'`]([^\"'`]*).*?,"],
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
],
"tailwindCSS.experimental.configFile": "src/styles/globals.css",
"typescript.experimental.useTsgo": true
"biome.enabled": true,
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"editor.defaultFormatter": "biomejs.biome",
"files.readonlyInclude": {
"**/locales/**.po": true,
"**/routeTree.gen.ts": true,
"pnpm-lock.yaml": true
},
"files.watcherExclude": {
"**/locales/**.po": true,
"**/routeTree.gen.ts": true,
"pnpm-lock.yaml": true
},
"search.exclude": {
"**/locales/**.po": true,
"**/routeTree.gen.ts": true,
"pnpm-lock.yaml": true
},
"tailwindCSS.classFunctions": ["cn", "cva"],
"tailwindCSS.experimental.classRegex": [
["class:\\s*?[\"'`]([^\"'`]*).*?,"],
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
],
"tailwindCSS.experimental.configFile": "src/styles/globals.css",
"typescript.experimental.useTsgo": true
}
-131
View File
@@ -1,131 +0,0 @@
## Overview
Reactive Resume is a single-package full-stack TypeScript app (not a monorepo) built with [TanStack Start](https://tanstack.com/start/latest/docs/framework/react/overview) (React, Vite, Nitro). It serves both frontend and API on port 3000.
This project uses [Vite+](https://vite.dev/blog/announcing-viteplus), a unified toolchain built on top of Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task. Vite+ wraps runtime management, package management, and frontend tooling in a single global CLI called `vp`. All modules should be imported from the `vite-plus` dependency (e.g., `import { defineConfig } from 'vite-plus'` or `import { expect, test, vi } from 'vite-plus/test'`).
## Key Libraries
| Area | Library | Docs |
| -------------------- | ------------------------------------------------------------------------ | ---------------------------------- |
| Frontend framework | React | https://react.dev |
| Full-stack framework | TanStack Start | https://tanstack.com/start/latest |
| Router | TanStack React Router | https://tanstack.com/router/latest |
| Server state | TanStack React Query | https://tanstack.com/query/latest |
| Client state | Zustand (+ Zundo for undo/redo, Immer for immutable updates) | https://zustand.docs.pmnd.rs |
| Type-safe API | oRPC | https://orpc.unnoq.com |
| Database ORM | Drizzle ORM (PostgreSQL) | https://orm.drizzle.team |
| Authentication | Better Auth (+ Drizzle adapter, OAuth provider, API keys, 2FA, Passkeys) | https://www.better-auth.com |
| Styling | Tailwind CSS | https://tailwindcss.com |
| UI Components | shadcn/ui (built on Base UI) | https://ui.shadcn.com |
| Icons | Phosphor Icons | https://phosphoricons.com |
| Forms | React Hook Form (+ Zod resolvers) | https://react-hook-form.com |
| Rich text editor | Tiptap | https://tiptap.dev |
| Validation | Zod | https://zod.dev |
| AI | Vercel AI SDK (OpenAI, Anthropic, Google, Ollama providers) | https://ai-sdk.dev |
| MCP | Model Context Protocol SDK | https://modelcontextprotocol.io |
| i18n | Lingui | https://lingui.dev |
| Animations | Motion (Framer Motion) | https://motion.dev |
| PDF export | Puppeteer Core (via Browserless) | https://pptr.dev |
| Drag and drop | dnd-kit | https://dndkit.com |
| Server engine | Nitro | https://nitro.build |
| PWA | Vite PWA Plugin | https://vite-pwa-org.netlify.app |
| Unused deps | Knip | https://knip.dev |
## Project Structure
```
src/
components/ UI, resume, layout, animation, theme, locale components
routes/ File-based routing (TanStack React Router)
integrations/ Feature modules (auth, drizzle, orpc, ai, email, jobs, mcp, storage)
schema/ Zod schemas for resume data validation
utils/ Utility functions (locale, theme, env, resume processing)
dialogs/ Modal/dialog components
hooks/ Custom React hooks
styles/ CSS and Tailwind configuration
stores/ Zustand stores (resume, AI, dialog, command palette)
migrations/ Drizzle database migrations
locales/ Lingui i18n message catalogs (47+ locales)
```
### Key Config Files
- `vite.config.ts` — Vite + Nitro + TanStack Start + PWA + Tailwind + Lingui
- `drizzle.config.ts` — PostgreSQL dialect, schema at `./src/integrations/drizzle/schema.ts`
- `tsconfig.json` — ES2022, strict mode, path alias `@/*` → `./src/*`
- `lingui.config.ts` — i18n extraction and locale configuration
- `components.json` — shadcn CLI configuration
### API Architecture
- **oRPC API** (`/api/rpc/*`) — Type-safe RPC with routers for: `ai`, `auth`, `resume`, `storage`, `printer`, `jobs`, `statistics`, `flags`. Three procedure types: `publicProcedure`, `protectedProcedure`, `serverOnlyProcedure`.
- **Better Auth API** (`/api/auth/*`) — OAuth, session management, social provider callbacks.
- **MCP Server** (`/mcp/`) — Model Context Protocol with OAuth Bearer tokens and API key auth. Exposes resumes as resources and tools for resume CRUD.
## 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
`vp` is the global CLI for Vite+. Do not use pnpm/npm/yarn directly — Vite+ wraps the underlying package manager.
| Task | Command |
| ----------------------------- | --------------------------------------------------------------- |
| Install dependencies | `vp install` |
| Dev server (port 3000) | `vp dev` |
| Lint (Oxlint, type-aware) | `vp lint --type-aware` |
| Format (Oxfmt) | `vp fmt` |
| Check (lint + format + types) | `vp check` |
| Typecheck | `pnpm typecheck` (uses tsgo) |
| Run tests | `vp test` |
| DB migrations | `pnpm db:generate` / `pnpm db:migrate` (auto-runs on dev start) |
| DB studio | `pnpm db:studio` |
| i18n extraction | `pnpm lingui:extract` |
| Add a dependency | `vp add <package>` |
| Remove a dependency | `vp remove <package>` |
| One-off binary | `vp dlx <package>` |
| Build for production | `vp build` |
| Preview production build | `vp preview` |
| Start production server | `pnpm start` |
## Vite+ Pitfalls
- **Do not use pnpm/npm/yarn directly** for package operations — use `vp add`, `vp remove`, `vp install`, etc.
- **Do not run `vp vitest` or `vp oxlint`** — they don't exist. Use `vp test` and `vp lint`.
- **Do not install Vitest, Oxlint, Oxfmt, or tsdown directly** — Vite+ bundles them.
- **Import from `vite-plus`**, not from `vite` or `vitest` directly (e.g., `import { defineConfig } from 'vite-plus'`).
- **Vite+ commands take precedence** over `package.json` scripts. If there's a naming conflict, use `vp run <script>`.
- **Use `vp dlx`** instead of `npx` or `pnpm dlx`.
- **Type-aware linting** works out of the box with `vp lint --type-aware` — no need to install `oxlint-tsgolint`.
## 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 and Nitro use beta/nightly builds. Occasional upstream issues may occur.
## Review Checklist for Agents
- [ ] Run `vp install` after pulling remote changes and before getting started.
- [ ] Run `pnpm lint:fix`, `pnpm format:fix`, `pnpm typecheck` and `vp test` to validate changes.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
+46 -37
View File
@@ -1,39 +1,44 @@
# syntax=docker/dockerfile:1
# syntax=docker/dockerfile:1.7
# ---------- Dependencies Layer ----------
FROM node:24-slim AS dependencies
ARG NODE_VERSION=24
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN mkdir -p /tmp/dev /tmp/prod
COPY package.json pnpm-lock.yaml /tmp/dev/
COPY package.json pnpm-lock.yaml /tmp/prod/
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
cd /tmp/dev && pnpm install --frozen-lockfile
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
cd /tmp/prod && pnpm install --frozen-lockfile --prod
# ---------- Builder Layer ----------
FROM node:24-slim AS builder
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM node:${NODE_VERSION}-slim AS base
WORKDIR /app
COPY --from=dependencies /tmp/dev/node_modules ./node_modules
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
PNPM_HOME="/pnpm" \
PATH="/pnpm:$PATH" \
TURBO_TELEMETRY_DISABLED=1
RUN corepack enable
FROM base AS pruner
COPY . .
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
pnpm dlx turbo@2.9.9 prune web --docker
RUN pnpm run build
FROM base AS builder
COPY --from=pruner /app/out/json/ ./
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
pnpm install --frozen-lockfile
# ---------- Runtime Layer ----------
FROM node:24-slim AS runtime
COPY --from=pruner /app/out/full/ ./
RUN pnpm turbo run build --filter=web
FROM base AS runtime-pruner
COPY . .
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
pnpm dlx turbo@2.9.9 prune @reactive-resume/runtime-externals --docker
FROM base AS runtime-deps
COPY --from=runtime-pruner /app/out/json/ ./
COPY --from=runtime-pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
pnpm --filter=@reactive-resume/runtime-externals deploy --prod --legacy /runtime-deps
FROM node:${NODE_VERSION}-slim AS runtime
LABEL maintainer="amruthpillai"
LABEL org.opencontainers.image.licenses="MIT"
@@ -44,20 +49,24 @@ 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/*
ENV NODE_ENV="production" \
PORT=3000 \
LOCAL_STORAGE_PATH=/app/data
WORKDIR /app
ENV NODE_ENV=production
RUN mkdir -p /app/apps/web /app/data && chown node:node /app/data
COPY --from=builder /app/.output ./.output
COPY --from=builder /app/migrations ./migrations
COPY --from=dependencies /tmp/prod/node_modules ./node_modules
COPY --from=runtime-deps --chown=node:node /runtime-deps/node_modules ./node_modules
COPY --from=builder --chown=node:node /app/apps/web/.output ./apps/web/.output
COPY --from=pruner --chown=node:node /app/migrations ./migrations
WORKDIR /app/apps/web
USER node
EXPOSE 3000/tcp
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:3000/api/health || exit 1
CMD ["node", "-e", "fetch('http://127.0.0.1:3000/api/health').then((r) => { if (!r.ok) process.exit(1); }).catch(() => process.exit(1));"]
ENTRYPOINT ["node", ".output/server/index.mjs"]
CMD ["node", ".output/server/index.mjs"]
-21
View File
@@ -1,21 +0,0 @@
# syntax=docker/dockerfile:1
# ---------- Development Layer ----------
FROM node:24-slim AS development
ENV NODE_ENV="development"
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
pnpm install --frozen-lockfile
EXPOSE 3000
CMD ["pnpm", "run", "dev"]
+21 -16
View File
@@ -1,6 +1,6 @@
<div align="center">
<a href="https://rxresu.me">
<img src="public/opengraph/banner.jpg" alt="Reactive Resume" />
<img src="apps/web/public/opengraph/banner.jpg" alt="Reactive Resume" />
</a>
<h1>Reactive Resume</h1>
@@ -14,7 +14,7 @@
</p>
<p>
<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/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" />
@@ -69,63 +69,67 @@ Built with privacy as a core principle, Reactive Resume gives you complete owner
<table>
<tr>
<td align="center">
<img src="public/templates/jpg/azurill.jpg" alt="Azurill" width="150" />
<img src="apps/web/public/templates/jpg/azurill.jpg" alt="Azurill" width="150" />
<br /><sub><b>Azurill</b></sub>
</td>
<td align="center">
<img src="public/templates/jpg/bronzor.jpg" alt="Bronzor" width="150" />
<img src="apps/web/public/templates/jpg/bronzor.jpg" alt="Bronzor" width="150" />
<br /><sub><b>Bronzor</b></sub>
</td>
<td align="center">
<img src="public/templates/jpg/chikorita.jpg" alt="Chikorita" width="150" />
<img src="apps/web/public/templates/jpg/chikorita.jpg" alt="Chikorita" width="150" />
<br /><sub><b>Chikorita</b></sub>
</td>
<td align="center">
<img src="public/templates/jpg/ditto.jpg" alt="Ditto" width="150" />
<img src="apps/web/public/templates/jpg/ditto.jpg" alt="Ditto" width="150" />
<br /><sub><b>Ditto</b></sub>
</td>
</tr>
<tr>
<td align="center">
<img src="public/templates/jpg/gengar.jpg" alt="Gengar" width="150" />
<img src="apps/web/public/templates/jpg/gengar.jpg" alt="Gengar" width="150" />
<br /><sub><b>Gengar</b></sub>
</td>
<td align="center">
<img src="public/templates/jpg/glalie.jpg" alt="Glalie" width="150" />
<img src="apps/web/public/templates/jpg/glalie.jpg" alt="Glalie" width="150" />
<br /><sub><b>Glalie</b></sub>
</td>
<td align="center">
<img src="public/templates/jpg/kakuna.jpg" alt="Kakuna" width="150" />
<img src="apps/web/public/templates/jpg/kakuna.jpg" alt="Kakuna" width="150" />
<br /><sub><b>Kakuna</b></sub>
</td>
<td align="center">
<img src="public/templates/jpg/lapras.jpg" alt="Lapras" width="150" />
<img src="apps/web/public/templates/jpg/lapras.jpg" alt="Lapras" width="150" />
<br /><sub><b>Lapras</b></sub>
</td>
</tr>
<tr>
<td align="center">
<img src="public/templates/jpg/leafish.jpg" alt="Leafish" width="150" />
<img src="apps/web/public/templates/jpg/leafish.jpg" alt="Leafish" width="150" />
<br /><sub><b>Leafish</b></sub>
</td>
<td align="center">
<img src="public/templates/jpg/onyx.jpg" alt="Onyx" width="150" />
<img src="apps/web/public/templates/jpg/onyx.jpg" alt="Onyx" width="150" />
<br /><sub><b>Onyx</b></sub>
</td>
<td align="center">
<img src="public/templates/jpg/pikachu.jpg" alt="Pikachu" width="150" />
<img src="apps/web/public/templates/jpg/pikachu.jpg" alt="Pikachu" width="150" />
<br /><sub><b>Pikachu</b></sub>
</td>
<td align="center">
<img src="public/templates/jpg/rhyhorn.jpg" alt="Rhyhorn" width="150" />
<img src="apps/web/public/templates/jpg/rhyhorn.jpg" alt="Rhyhorn" width="150" />
<br /><sub><b>Rhyhorn</b></sub>
</td>
</tr>
<tr>
<td align="center">
<img src="public/templates/jpg/ditgar.jpg" alt="Ditgar" width="150" />
<img src="apps/web/public/templates/jpg/ditgar.jpg" alt="Ditgar" width="150" />
<br /><sub><b>Ditgar</b></sub>
</td>
<td align="center">
<img src="apps/web/public/templates/jpg/meowth.jpg" alt="Meowth" width="150" />
<br /><sub><b>Meowth</b></sub>
</td>
</tr>
</table>
@@ -180,9 +184,10 @@ 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
- **Printer** — Headless Chromium service for PDF and screenshot generation
- **SeaweedFS** (optional) — S3-compatible storage for file uploads
> **From v5.1.0 onwards** — PDF generation now runs entirely client-side via `@react-pdf/renderer`. New deployments no longer require Browserless, Chromium, or any external print service as a dependency. The `PRINTER_*` and `BROWSERLESS_*` environment variables are no longer read and can be removed from your `.env`.
Pull the latest image from Docker Hub or GitHub Container Registry:
```bash
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "../../packages/ui/src/styles/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "phosphor",
"aliases": {
"components": "@reactive-resume/ui/components",
"utils": "@reactive-resume/utils/style",
"hooks": "@reactive-resume/ui/hooks",
"lib": "@reactive-resume/utils",
"ui": "@reactive-resume/ui/components"
},
"rtl": true,
"menuColor": "default-translucent",
"menuAccent": "subtle",
"registries": {}
}
+77
View File
@@ -0,0 +1,77 @@
import { defineConfig } from "@lingui/cli";
import { formatter } from "@lingui/format-po";
export default defineConfig({
sourceLocale: "en-US",
pseudoLocale: "zu-ZA",
locales: [
"af-ZA",
"am-ET",
"ar-SA",
"az-AZ",
"bg-BG",
"bn-BD",
"ca-ES",
"cs-CZ",
"da-DK",
"de-DE",
"el-GR",
"en-US",
"en-GB",
"es-ES",
"fa-IR",
"fi-FI",
"fr-FR",
"he-IL",
"hi-IN",
"hu-HU",
"id-ID",
"it-IT",
"ja-JP",
"km-KH",
"kn-IN",
"ko-KR",
"lt-LT",
"lv-LV",
"ml-IN",
"mr-IN",
"ms-MY",
"ne-NP",
"nl-NL",
"no-NO",
"or-IN",
"pl-PL",
"pt-BR",
"pt-PT",
"ro-RO",
"ru-RU",
"sk-SK",
"sl-SI",
"sq-AL",
"sr-SP",
"sv-SE",
"ta-IN",
"te-IN",
"th-TH",
"tr-TR",
"uk-UA",
"uz-UZ",
"vi-VN",
"zh-CN",
"zh-TW",
"zu-ZA",
],
fallbackLocales: {
"zu-ZA": "en-US",
default: "en-US",
},
format: formatter({
lineNumbers: false,
}),
catalogs: [
{
path: "<rootDir>/locales/{locale}",
include: ["src"],
},
],
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+109
View File
@@ -0,0 +1,109 @@
{
"name": "web",
"version": "0.0.0",
"type": "module",
"private": true,
"scripts": {
"build": "vite build",
"dev": "vite dev",
"serve": "vite preview",
"start": "node .output/server/index.mjs",
"typecheck": "tsgo --noEmit",
"test": "vitest run --passWithNoTests",
"test:coverage": "vitest run --coverage --passWithNoTests",
"test:ci": "vitest run --coverage --reporter=default --reporter=github-actions --reporter=json --reporter=junit --outputFile.json=reports/vitest-results.json --outputFile.junit=reports/vitest-junit.xml --passWithNoTests",
"test:agent": "vitest run --reporter=agent --reporter=json --outputFile.json=reports/vitest-results.json --passWithNoTests",
"lingui:extract": "lingui extract --clean --overwrite"
},
"dependencies": {
"@ai-sdk/react": "^3.0.179",
"@base-ui/react": "^1.4.1",
"@better-auth/api-key": "^1.6.10",
"@better-auth/infra": "^0.2.6",
"@better-auth/oauth-provider": "^1.6.10",
"@better-auth/passkey": "^1.6.10",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@lingui/core": "^6.0.1",
"@lingui/react": "^6.0.1",
"@modelcontextprotocol/sdk": "^1.29.0",
"@orpc/client": "^1.14.2",
"@orpc/json-schema": "^1.14.2",
"@orpc/openapi": "^1.14.2",
"@orpc/server": "^1.14.2",
"@orpc/tanstack-query": "^1.14.2",
"@orpc/zod": "^1.14.2",
"@phosphor-icons/react": "^2.1.10",
"@react-pdf/renderer": "^4.5.1",
"@reactive-resume/ai": "workspace:*",
"@reactive-resume/api": "workspace:*",
"@reactive-resume/auth": "workspace:*",
"@reactive-resume/db": "workspace:*",
"@reactive-resume/env": "workspace:*",
"@reactive-resume/fonts": "workspace:*",
"@reactive-resume/import": "workspace:*",
"@reactive-resume/pdf": "workspace:*",
"@reactive-resume/schema": "workspace:*",
"@reactive-resume/ui": "workspace:*",
"@reactive-resume/utils": "workspace:*",
"@tailwindcss/vite": "^4.3.0",
"@tanstack/react-form": "^1.31.0",
"@tanstack/react-hotkeys": "^0.10.0",
"@tanstack/react-query": "^5.100.9",
"@tanstack/react-router": "^1.169.2",
"@tanstack/react-router-ssr-query": "^1.166.12",
"@tanstack/react-start": "^1.167.65",
"@tiptap/extension-color": "^3.23.1",
"@tiptap/extension-highlight": "^3.23.1",
"@tiptap/extension-table": "^3.23.1",
"@tiptap/extension-text-align": "^3.23.1",
"@tiptap/extension-text-style": "^3.23.1",
"@tiptap/pm": "^3.23.1",
"@tiptap/react": "^3.23.1",
"@tiptap/starter-kit": "^3.23.1",
"@types/js-cookie": "^3.0.6",
"@uiw/color-convert": "^2.10.1",
"@uiw/react-color-colorful": "^2.10.1",
"better-auth": "1.6.10",
"cmdk": "^1.1.1",
"drizzle-orm": "1.0.0-beta.22",
"es-toolkit": "^1.46.1",
"fuse.js": "^7.3.0",
"immer": "^11.1.8",
"js-cookie": "^3.0.5",
"motion": "^12.38.0",
"pdfjs-dist": "5.7.284",
"pg": "^8.20.0",
"qrcode.react": "^4.2.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-resizable-panels": "^4.11.0",
"react-window": "^2.2.7",
"react-zoom-pan-pinch": "^4.0.3",
"sonner": "^2.0.7",
"srvx": "^0.11.15",
"ts-pattern": "^5.9.0",
"usehooks-ts": "^3.1.1",
"zod": "^4.4.3",
"zustand": "^5.0.13"
},
"devDependencies": {
"@lingui/babel-plugin-lingui-macro": "^6.0.1",
"@lingui/cli": "^6.0.1",
"@lingui/format-po": "^6.0.1",
"@lingui/vite-plugin": "^6.0.1",
"@reactive-resume/config": "workspace:*",
"@rolldown/plugin-babel": "^0.2.3",
"@types/pg": "^8.20.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@typescript/native-preview": "7.0.0-dev.20260510.1",
"@vitejs/plugin-react": "^6.0.1",
"babel-plugin-macros": "^3.1.0",
"nitro": "3.0.260429-beta",
"typescript": "^6.0.3",
"vite": "^8.0.11",
"vite-plugin-pwa": "^1.3.0"
}
}
+40
View File
@@ -0,0 +1,40 @@
import { existsSync } from "node:fs";
import path from "node:path";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import { definePlugin } from "nitro";
import { Pool } from "pg";
import { env } from "@reactive-resume/env/server";
function resolveMigrationsFolder(): string {
let dir = import.meta.dirname;
while (dir !== path.dirname(dir)) {
const candidate = path.join(dir, "migrations");
if (existsSync(candidate)) return candidate;
dir = path.dirname(dir);
}
throw new Error(`Could not locate migrations folder relative to ${import.meta.dirname}`);
}
const migrationsFolder = resolveMigrationsFolder();
export default definePlugin(async () => {
console.info("Running database migrations...");
const connectionString = env.DATABASE_URL;
const pool = new Pool({ connectionString });
const db = drizzle({ client: pool });
try {
await migrate(db, { migrationsFolder });
console.info("Database migrations completed");
} catch (error) {
console.error("Database migrations failed", { error });
throw error;
} finally {
await pool.end();
}
});
+24
View File
@@ -0,0 +1,24 @@
import fs from "node:fs/promises";
import { definePlugin } from "nitro";
import { env } from "@reactive-resume/env/server";
import { getLocalDataDirectory } from "@reactive-resume/utils/monorepo.node";
export default definePlugin(async () => {
if (env.S3_ACCESS_KEY_ID && env.S3_SECRET_ACCESS_KEY && env.S3_BUCKET) return;
const dataDirectory = getLocalDataDirectory(env.LOCAL_STORAGE_PATH);
console.info(`Validating local storage path: ${dataDirectory}`);
try {
await fs.mkdir(dataDirectory, { recursive: true });
await fs.access(dataDirectory, fs.constants.R_OK | fs.constants.W_OK);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error(
`Local storage path is not writable: ${dataDirectory}\n` +
` ${message}\n` +
"Set LOCAL_STORAGE_PATH to a writable directory or fix permissions on the existing path.",
);
throw error;
}
});

Before

Width:  |  Height:  |  Size: 958 B

After

Width:  |  Height:  |  Size: 958 B

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
+68
View File
@@ -0,0 +1,68 @@
{
"$schema": "https://fundingjson.org/schema/v1.1.0.json",
"version": "v1.1.0",
"entity": {
"type": "individual",
"role": "maintainer",
"name": "Amruth Pillai",
"email": "hello@amruthpillai.com",
"description": "Software Engineer",
"webpageUrl": {
"url": "https://rxresu.me/funding.json"
}
},
"projects": [
{
"guid": "reactive-resume",
"name": "Reactive Resume",
"description": "A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.",
"webpageUrl": {
"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"
},
"licenses": ["spdx:MIT"],
"tags": ["data", "design", "productivity", "resume-builder"]
}
],
"funding": {
"plans": [
{
"guid": "sponsor",
"status": "active",
"name": "Sponsor",
"description": "Support the project on a recurring basis by becoming a sponsor.",
"amount": 10,
"currency": "EUR",
"frequency": "monthly",
"channels": ["github", "open-collective"]
},
{
"guid": "donation",
"status": "active",
"name": "Donation",
"description": "Show your support for the project by making a one-time donation.",
"amount": 0,
"currency": "EUR",
"frequency": "one-time",
"channels": ["github", "open-collective"]
}
],
"channels": [
{
"guid": "github",
"type": "payment-provider",
"description": "GitHub Sponsors",
"address": "https://github.com/sponsors/AmruthPillai"
},
{
"guid": "open-collective",
"type": "payment-provider",
"description": "Open Collective",
"address": "https://opencollective.com/reactive-resume"
}
]
}
}

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

+98
View File
@@ -0,0 +1,98 @@
{
"name": "Reactive Resume",
"short_name": "Reactive Resume",
"description": "A free and open-source resume builder.",
"id": "/?source=pwa",
"start_url": "/?source=pwa",
"scope": "/",
"lang": "en",
"display": "standalone",
"orientation": "portrait",
"theme_color": "#09090B",
"background_color": "#09090B",
"icons": [
{ "src": "favicon.ico", "sizes": "128x128", "type": "image/x-icon" },
{ "src": "pwa-64x64.png", "sizes": "64x64", "type": "image/png" },
{ "src": "pwa-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "pwa-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
{ "src": "maskable-icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
],
"screenshots": [
{
"src": "screenshots/web/1-landing-page.webp",
"sizes": "1920x1080 any",
"type": "image/webp",
"form_factor": "wide",
"label": "Landing Page"
},
{
"src": "screenshots/web/2-resume-dashboard.webp",
"sizes": "1920x1080 any",
"type": "image/webp",
"form_factor": "wide",
"label": "Resume Dashboard"
},
{
"src": "screenshots/web/3-builder-screen.webp",
"sizes": "1920x1080 any",
"type": "image/webp",
"form_factor": "wide",
"label": "Builder Screen"
},
{
"src": "screenshots/web/4-template-gallery.webp",
"sizes": "1920x1080 any",
"type": "image/webp",
"form_factor": "wide",
"label": "Template Gallery"
},
{
"src": "screenshots/mobile/1-landing-page.webp",
"sizes": "1284x2778 any",
"type": "image/webp",
"form_factor": "narrow",
"label": "Landing Page"
},
{
"src": "screenshots/mobile/2-resume-dashboard.webp",
"sizes": "1284x2778 any",
"type": "image/webp",
"form_factor": "narrow",
"label": "Resume Dashboard"
},
{
"src": "screenshots/mobile/3-builder-screen.webp",
"sizes": "1284x2778 any",
"type": "image/webp",
"form_factor": "narrow",
"label": "Builder Screen"
},
{
"src": "screenshots/mobile/4-template-gallery.webp",
"sizes": "1284x2778 any",
"type": "image/webp",
"form_factor": "narrow",
"label": "Template Gallery"
}
],
"categories": [
"ai",
"builder",
"business",
"career",
"cv",
"editor",
"free",
"generator",
"job-search",
"multilingual",
"open-source",
"privacy",
"productivity",
"resume",
"self-hosted",
"templates",
"utilities",
"writing"
]
}

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 385 KiB

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 483 B

After

Width:  |  Height:  |  Size: 483 B

Some files were not shown because too many files have changed in this diff Show More