diff --git a/.github/workflows/vercel.yml b/.github/workflows/vercel.yml
index 5593c0035..78c47c0de 100644
--- a/.github/workflows/vercel.yml
+++ b/.github/workflows/vercel.yml
@@ -58,29 +58,35 @@ jobs:
import { writeFileSync } from 'node:fs';
writeFileSync('.vercel/project.json', JSON.stringify({
projectId: 'prj_ci', orgId: 'team_ci', projectName: 'reactive-resume-ci',
- settings: { framework: null, nodeVersion: '24.x', createdAt: 0 }
+ settings: { framework: 'services', nodeVersion: '24.x', createdAt: 0 }
}));
JS
- pnpm dlx --allow-build=esbuild vercel@60.0.1 build --prod --yes --global-config "$RUNNER_TEMP/vercel-offline"
- - name: Check Lambda module loading and function budget
+ pnpm dlx --allow-build=esbuild vercel@61.0.0 build --prod --yes --global-config "$RUNNER_TEMP/vercel-offline"
+ # Runs the backend Function from a copy outside the checkout, so a dependency the build left out fails here.
+ - name: Check backend Function loading and budget
run: |
node --no-experimental-require-module --input-type=module - <<'JS'
import assert from 'node:assert/strict';
- import { readFileSync, readdirSync } from 'node:fs';
- const config = JSON.parse(readFileSync('.vercel/output/functions/api/index.func/.vc-config.json'));
+ import { cpSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync } from 'node:fs';
+ import { dirname, join } from 'node:path';
+ const func = '.vercel/output/services/backend/functions/index.func';
+ const config = JSON.parse(readFileSync(`${func}/.vc-config.json`));
assert.equal(config.runtime, 'nodejs24.x');
assert.equal(config.maxDuration, 300);
- const tracedFiles = Object.keys(config.filePathMap ?? {});
- assert.ok(tracedFiles.some((path) => path.endsWith('/pdfkit/js/standard-fonts/Helvetica.cjs')));
- assert.ok(tracedFiles.some((path) => path.endsWith('/pdfkit/js/data/Helvetica.afm')));
- for (const name of readdirSync('apps/server/dist')) {
- if (name.endsWith('.mjs') && !['index.mjs', 'prepare-deployment.mjs'].includes(name)) {
- await import(`./apps/server/dist/${name}`);
- }
+ assert.equal(config.handler, 'apps/server/vercel.mjs');
+ const root = join(process.env.RUNNER_TEMP, 'backend-function');
+ cpSync(func, root, { recursive: true, verbatimSymlinks: true });
+ for (const [path, source] of Object.entries(config.filePathMap ?? {})) {
+ mkdirSync(dirname(join(root, path)), { recursive: true });
+ if (lstatSync(source).isSymbolicLink()) symlinkSync(readlinkSync(source), join(root, path));
+ else cpSync(source, join(root, path));
}
- const { default: app } = await import('./apps/server/dist/vercel.mjs');
- const response = await app.fetch(new Request('http://localhost:3000/api/storage/stage', { method: 'POST', body: '{}' }));
- assert.equal(response.status, 401);
+ const { default: app } = await import(join(root, config.handler));
+ const stage = await app.fetch(new Request('http://localhost:3000/api/storage/stage', { method: 'POST', body: '{}' }));
+ assert.equal(stage.status, 401);
+ const home = await app.fetch(new Request('http://localhost:3000/'));
+ assert.equal(home.status, 200);
+ assert.match(await home.text(), /application\/ld\+json/);
process.exit(0);
JS
diff --git a/AGENTS.md b/AGENTS.md
index 32a87efe7..fcee86f95 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -52,7 +52,7 @@ This block is written and re-added by `turbo` before repository-scoped commands
Reactive Resume is a free, open-source resume builder for creating, importing, exporting, and sharing resumes, cover letters, and job applications. It is a TypeScript pnpm monorepo managed by Turborepo, with two apps: `apps/web` (React 19 SPA with TanStack Router, TanStack Query, Tailwind CSS, and Vite) and `apps/server` (Hono / Node.js). oRPC connects browser workflows to server business logic; Better Auth handles authentication; Drizzle accesses PostgreSQL. Forme renders PDFs in the browser and on the server.
-The production Docker image runs a single Node.js process on port 3000; `apps/server` mounts the API/auth/MCP/static routes and serves the built web app. Vercel serves static assets through its CDN and runs the same Hono application in a Node.js Function.
+The production Docker image runs a single Node.js process on port 3000; `apps/server` mounts the API/auth/MCP/static routes and serves the built web app. On Vercel, the `frontend` service serves static assets through its CDN and the `backend` service runs the same Hono application in a Node.js Function.
Internal packages are source-consumed through `package.json` export maps pointing at `src` files. Do not assume package-local `dist` output exists unless a package explicitly adds it.
@@ -212,7 +212,7 @@ docker compose up -d --build
- Build outputs: `apps/web/dist` (SPA/assets), `apps/web/dist-prerender` (localized marketing HTML), and `apps/server/dist` (`index.mjs` plus server/deployment chunks). `pnpm start` runs the built server; set `NODE_ENV=production` so it uses `PORT` instead of `SERVER_PORT`. Export runtime variables or provide root `.env`; `.env.local` is not loaded by `start`.
- Production Compose loads `.env.example` then `.env`, not `.env.local`. Configure `.env` with container hostnames (`postgres`, `redis`, `seaweedfs`) and production secrets before running it. The Docker image runs as `node`, listens on `3000`, and persists local storage through `/app/data`. Health endpoint: `/api/health`.
-- `vercel.json` runs `pnpm build` then `node apps/server/dist/prepare-deployment.mjs`, serves `apps/web/dist`, and routes dynamic requests to `api/index.mjs`. The Function uses Node 24 and a 300-second budget. Preserve traced PDFKit font assets and deployment chunks when changing bundling.
+- `vercel.json` defines Vercel Services (project framework must be `Services`): `frontend` (`apps/web`, static `dist`) and `backend` (`apps/server`, entrypoint `apps/server/vercel.mjs` re-exporting the tsdown build). The backend build runs `pnpm build` for both apps, then `node apps/server/dist/prepare-deployment.mjs`. Top-level rewrites send paths whose last segment has a file extension to `frontend` and everything else, including HTML shells, to `backend`, except the server-owned paths listed first. The Function uses Node 24 and a 300-second budget. `outputDirectory: "."` on `backend` stops the builder from treating `dist/index.mjs` (the Docker entrypoint) as the handler.
- Vercel environment normalization accepts `POSTGRES_URL`, direct/unpooled DB aliases, and `KV_URL`. `APP_URL` can be derived from Vercel host variables. Blob is the default when no S3 credentials are set. Preview deployments require isolated resources before enabling `ALLOW_PREVIEW_MIGRATIONS=true`; see `docs/self-hosting/vercel.mdx`.
- `.github/workflows/e2e.yml` gates core unit/browser flows; `vercel.yml` builds and checks the serverless artifact on PRs and pushes to `main`. `autofix.yml` runs write-capable `pnpm knip --fix` and `pnpm check`. GitHub runners are the default; `USE_BLACKSMITH=true` switches runners and paired actions.
- `docker-build.yml` publishes native AMD64/ARM64 images. `main` publishes nightly aliases; release tags/explicit release dispatch publish stable aliases and can trigger configured production integrations. See `docs/agents/container-publishing.md` before release work.
@@ -233,5 +233,5 @@ docker compose up -d --build
- Database connection errors: check `docker compose -f compose.dev.yml ps` and use `localhost` for host-run code, service names inside containers.
- S3 errors: check `docker compose -f compose.dev.yml logs seaweedfs seaweedfs_create_bucket`; verify endpoint and bucket, or select local storage.
- Route-tree errors after adding routes: run Vite dev/build to regenerate `apps/web/src/routeTree.gen.ts`; never edit it by hand.
-- Serverless module-loading failures: inspect `bundledInteropPackages` in `apps/server/tsdown.config.ts` and the Vercel compatibility workflow.
+- Serverless module-loading failures: inspect `bundledInteropPackages` in `apps/server/tsdown.config.ts` and the Vercel compatibility workflow. External CommonJS server dependencies break on Vercel because its service builder drops their pnpm links; bundle them with their dependencies.
- Most test scripts use `--passWithNoTests`; a successful run with zero tests does not verify the behavior you changed.
diff --git a/api/index.mjs b/api/index.mjs
deleted file mode 100644
index bb5ac4e79..000000000
--- a/api/index.mjs
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "../apps/server/dist/vercel.mjs";
diff --git a/apps/server/package.json b/apps/server/package.json
index b2f0efdac..5ae7be590 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -58,7 +58,7 @@
"@vercel/blob": "^2.8.0",
"@vercel/functions": "^3.9.9",
"ai": "^7.0.122",
- "bcrypt": "^6.0.0",
+ "bcryptjs": "^3.0.3",
"better-auth": "1.7.6",
"cjk-regex": "^3.5.0",
"css-tree": "^3.2.1",
diff --git a/apps/server/tsdown.config.ts b/apps/server/tsdown.config.ts
index a8222a3c0..773035f4e 100644
--- a/apps/server/tsdown.config.ts
+++ b/apps/server/tsdown.config.ts
@@ -8,8 +8,20 @@ const rootPackageJson = JSON.parse(readFileSync(new URL("../../package.json", im
version?: string;
};
-// Lambda disables require(ESM) and uses stricter CJS export detection than standalone Node.
+// Lambda disables require(ESM) and uses stricter CJS export detection than standalone Node. Vercel's service builder
+// also loads external CommonJS packages through pnpm links it leaves out of the Function, so CommonJS dependencies
+// (ioredis, react-reconciler) are bundled together with their own dependencies.
const bundledInteropPackages = new Set([
+ "ioredis",
+ "@ioredis/commands",
+ "cluster-key-slot",
+ "debug",
+ "ms",
+ "denque",
+ "redis-errors",
+ "standard-as-callback",
+ "react-reconciler",
+ "scheduler",
"@uiw/color-convert",
"@babel/runtime",
"sanitize-html",
@@ -31,12 +43,18 @@ const bundledInteropPackages = new Set([
"dayjs",
]);
-const shouldExternalizeThirdParty = (id: string) => {
- const packageName = id
+const packageNameOf = (id: string) =>
+ id
.split("/")
.slice(0, id.startsWith("@") ? 2 : 1)
.join("/");
- if (id.startsWith("@reactive-resume/") || bundledInteropPackages.has(packageName)) return false;
+
+// Matches subpath imports too, such as `react-reconciler/constants.js`.
+const shouldBundle = (id: string) =>
+ id.startsWith("@reactive-resume/") || bundledInteropPackages.has(packageNameOf(id));
+
+const shouldExternalizeThirdParty = (id: string) => {
+ if (shouldBundle(id)) return false;
// Subpath imports (`#…`) are resolved by the workspace package that declares them, so they're bundled with it.
if (id.startsWith("@/") || id.startsWith("#") || id.startsWith(".") || id.startsWith("/") || id.startsWith("\0"))
return false;
@@ -83,7 +101,7 @@ export default defineConfig({
suppressWarnings: [/dynamic import will not move module into another chunk/],
outExtensions: () => ({ js: ".mjs" }),
deps: {
- alwaysBundle: [/^@reactive-resume\//, ...bundledInteropPackages],
+ alwaysBundle: shouldBundle,
neverBundle: shouldExternalizeThirdParty,
},
plugins: [promptAssetsPlugin],
diff --git a/apps/server/vercel.mjs b/apps/server/vercel.mjs
new file mode 100644
index 000000000..5d6d4c20b
--- /dev/null
+++ b/apps/server/vercel.mjs
@@ -0,0 +1,2 @@
+// Vercel service entrypoint. It must exist before the build, so it re-exports the adapter that tsdown emits.
+export { default } from "./dist/vercel.mjs";
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index da2945df1..a3114d6ff 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -101,7 +101,7 @@ export default defineConfig({
build: {
chunkSizeWarningLimit: 10 * 1024, // 10 MB
rolldownOptions: {
- external: ["bcrypt", "sharp", "@aws-sdk/client-s3", "ioredis", "linkedom"],
+ external: ["bcryptjs", "sharp", "@aws-sdk/client-s3", "ioredis", "linkedom"],
},
},
diff --git a/docs/contributing/architecture.mdx b/docs/contributing/architecture.mdx
index 3ee879a80..2ff06ccef 100644
--- a/docs/contributing/architecture.mdx
+++ b/docs/contributing/architecture.mdx
@@ -3,7 +3,7 @@ title: "Project architecture"
description: "How the Reactive Resume monorepo is laid out, the runtime boundaries between the web and server apps, and the package ownership model."
---
-Reactive Resume is a pnpm/Turborepo monorepo. Docker runs one Node.js process. Vercel serves static assets through its CDN and uses a Node.js Function for the same Hono application. Both targets share the web app, API, authentication, renderers, and database schema.
+Reactive Resume is a pnpm/Turborepo monorepo. Docker runs one Node.js process. Vercel deploys two services from one project: `frontend` serves static assets through its CDN, and `backend` runs the same Hono application in a Node.js Function. Both targets share the web app, API, authentication, renderers, and database schema.
Internal packages are source-consumed through their `package.json` export maps. Import package subpaths, not another workspace's private `src` files.
diff --git a/docs/contributing/deployment-checks.mdx b/docs/contributing/deployment-checks.mdx
index a6ba416f6..8b937c404 100644
--- a/docs/contributing/deployment-checks.mdx
+++ b/docs/contributing/deployment-checks.mdx
@@ -12,14 +12,13 @@ Runs on every pull request and every push to `main`. It needs no Vercel account
The job:
1. Starts an isolated PostgreSQL service.
-2. Writes a local `.vercel/project.json` and runs `vercel build --prod` offline, with placeholder Blob credentials. This also applies migrations to the isolated database.
-3. Checks the generated Function:
- - runtime is `nodejs24.x` and `maxDuration` is `300`;
- - PDFKit standard font files are included in the traced files;
- - every emitted server chunk loads with `--no-experimental-require-module`, which matches the Vercel runtime;
- - the Vercel entrypoint answers a request.
+2. Writes a local `.vercel/project.json` with the `services` framework and runs `vercel build --prod` offline, with placeholder Blob credentials. This also applies migrations to the isolated database.
+3. Checks the `backend` service Function:
+ - runtime is `nodejs24.x`, `maxDuration` is `300`, and the handler is `apps/server/vercel.mjs`;
+ - a copy of the Function outside the checkout loads with `--no-experimental-require-module`, which matches the Vercel runtime, so a dependency the build left out fails the job;
+ - the copy rejects an unauthenticated staging request and serves the prerendered homepage.
-If a new server dependency fails the module-loading check, add it to `bundledInteropPackages` in `apps/server/tsdown.config.ts`.
+If a new server dependency fails the loading check, add it and its own dependencies to `bundledInteropPackages` in `apps/server/tsdown.config.ts`. CommonJS dependencies need this: Vercel's service builder loads them through pnpm links that it leaves out of the Function.
## Live smoke test
diff --git a/docs/self-hosting/vercel.mdx b/docs/self-hosting/vercel.mdx
index 6ca521c02..ceb4a155c 100644
--- a/docs/self-hosting/vercel.mdx
+++ b/docs/self-hosting/vercel.mdx
@@ -5,7 +5,7 @@ description: "Deploy Reactive Resume on Vercel Hobby with Neon PostgreSQL, priva
This guide deploys Reactive Resume to a Vercel project with the Deploy with Vercel wizard. The wizard provisions a database, file storage, and Redis for you. You supply two secrets.
-Vercel serves the static web assets from its CDN and runs the shared Hono server in one Node.js 24 Function. Docker is also supported and uses the same API, authentication, templates, and data format. See [Self-hosting with Docker](/self-hosting/docker) for that option.
+The project deploys as two [Vercel Services](https://vercel.com/docs/services): `frontend` serves the static web app from Vercel's CDN, and `backend` runs the shared Hono server in one Node.js 24 Function. Docker is also supported and uses the same API, authentication, templates, and data format. See [Self-hosting with Docker](/self-hosting/docker) for that option.
## Before you start
@@ -56,7 +56,7 @@ Quotas and permitted use depend on your Vercel, Neon, and Upstash plans. Review
- Keep the project root at the repository root and keep the committed `vercel.json`. Do not select the `apps/web` subdirectory and do not add an SPA fallback rewrite.
+ Set **Framework Preset** to **Services**. Keep the project root at the repository root and keep the committed `vercel.json`. Do not select the `apps/web` subdirectory and do not add an SPA fallback rewrite.
The build compiles both apps and applies database migrations before the deployment goes live. You do not run migrations yourself.
@@ -87,6 +87,10 @@ Changing the domain does not move stored files. Files stay under the same `DEPLO
Redeploy the same project. Keep its connected services and secrets unchanged.
+
+ Installations created before Reactive Resume used Vercel Services must switch first. Open **Settings → Build and Deployment**, set **Framework Preset** to **Services**, save, and then redeploy. Vercel builds the `services` configuration only when the project uses this preset.
+
+
- Migrations run in the build step, never in runtime Functions. A database advisory lock serializes concurrent deployments.
- Rolling back to an older deployment does not roll back the database schema. Keep migrations backward-compatible, or restore a database backup.
@@ -151,6 +155,7 @@ Blob objects are never public. The application serves public pictures and author
| Symptom | Fix |
| --- | --- |
+| Deployment does not build as services | Set **Framework Preset** to **Services** under **Settings → Build and Deployment**, then redeploy. |
| First build fails on configuration | Check that all three services are connected to Production and both secrets are set. Remove any `S3_*` variables and any `STORAGE_BACKEND` value other than `blob`. |
| Preview build refuses to migrate | Connect isolated preview resources, then set `ALLOW_PREVIEW_MIGRATIONS=true` for Preview. |
| Large upload returns `413` | Use the web app or the [large RPC requests](/guides/large-rpc-requests) protocol. Vercel Pro does not raise the request body limit. |
diff --git a/knip.json b/knip.json
index fcc1a75f6..3530c1d08 100644
--- a/knip.json
+++ b/knip.json
@@ -2,9 +2,6 @@
"$schema": "https://unpkg.com/knip@6/schema.json",
"tags": ["-lintignore"],
"workspaces": {
- ".": {
- "entry": ["api/index.mjs"]
- },
"apps/web": {
"entry": ["lingui.config.ts", "vite.config.ts", "vitest.config.ts", "src/features/homepage/prerender.tsx"],
"vite": {
@@ -15,6 +12,7 @@
}
},
"apps/server": {
+ "entry": ["vercel.mjs"],
"ignoreDependencies": [
"@ai-sdk/anthropic",
"@ai-sdk/cerebras",
@@ -43,7 +41,7 @@
"@uiw/color-convert",
"@vercel/blob",
"ai",
- "bcrypt",
+ "bcryptjs",
"cjk-regex",
"css-tree",
"drizzle-zod",
diff --git a/packages/api/package.json b/packages/api/package.json
index 6d02fd881..c95b0ce08 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -51,7 +51,7 @@
"@reactive-resume/utils": "workspace:*",
"@vercel/blob": "^2.8.0",
"ai": "^7.0.122",
- "bcrypt": "^6.0.0",
+ "bcryptjs": "^3.0.3",
"better-auth": "1.7.6",
"drizzle-orm": "1.0.0-rc.4",
"drizzle-zod": "1.0.0-beta.14-a36c63d",
@@ -68,7 +68,6 @@
},
"devDependencies": {
"@reactive-resume/config": "workspace:*",
- "@types/bcrypt": "^6.0.0",
"@types/pg": "^8.23.1",
"@types/sanitize-html": "^2.16.2",
"@typescript/native-preview": "7.0.0-dev.20260707.2",
diff --git a/packages/api/src/features/resume/service.test.ts b/packages/api/src/features/resume/service.test.ts
index ddf871900..6333a47d2 100644
--- a/packages/api/src/features/resume/service.test.ts
+++ b/packages/api/src/features/resume/service.test.ts
@@ -66,7 +66,7 @@ vi.mock("drizzle-orm", () => ({
join: (values: unknown[]) => values,
}),
}));
-vi.mock("bcrypt", () => ({ hash: vi.fn(), compare: compareMock }));
+vi.mock("bcryptjs", () => ({ hash: vi.fn(), compare: compareMock }));
vi.mock("./events", () => ({ publishResumeUpdated: publishResumeUpdatedMock }));
vi.mock("./access", () => ({
grantResumeAccess: grantResumeAccessMock,
diff --git a/packages/api/src/features/resume/service.ts b/packages/api/src/features/resume/service.ts
index 5a456aac5..71570801d 100644
--- a/packages/api/src/features/resume/service.ts
+++ b/packages/api/src/features/resume/service.ts
@@ -4,7 +4,7 @@ import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { Locale } from "@reactive-resume/utils/locale";
import type { ResumeUpdatedEvent } from "./events";
import { ORPCError } from "@orpc/client";
-import { compare, hash } from "bcrypt";
+import { compare, hash } from "bcryptjs";
import { and, arrayContains, asc, desc, eq, gte, isNotNull, isNull, sql } from "drizzle-orm";
import { match } from "ts-pattern";
import { db } from "@reactive-resume/db/client";
diff --git a/packages/auth/package.json b/packages/auth/package.json
index 33f932fab..7607ee3bf 100644
--- a/packages/auth/package.json
+++ b/packages/auth/package.json
@@ -24,7 +24,7 @@
"@reactive-resume/email": "workspace:*",
"@reactive-resume/env": "workspace:*",
"@reactive-resume/utils": "workspace:*",
- "bcrypt": "^6.0.0",
+ "bcryptjs": "^3.0.3",
"better-auth": "1.7.6",
"drizzle-orm": "1.0.0-rc.4",
"jose": "^6.2.12",
@@ -32,7 +32,6 @@
},
"devDependencies": {
"@reactive-resume/config": "workspace:*",
- "@types/bcrypt": "^6.0.0",
"@types/react": "^19.3.0",
"@typescript/native-preview": "7.0.0-dev.20260707.2",
"typescript": "^7.0.2"
diff --git a/packages/auth/src/config.ts b/packages/auth/src/config.ts
index 8ecc2c118..76a85fc77 100644
--- a/packages/auth/src/config.ts
+++ b/packages/auth/src/config.ts
@@ -5,7 +5,7 @@ import { drizzleAdapter } from "@better-auth/drizzle-adapter";
import { dash } from "@better-auth/infra";
import { oauthProvider } from "@better-auth/oauth-provider";
import { passkey } from "@better-auth/passkey";
-import { compare, hash } from "bcrypt";
+import { compare, hash } from "bcryptjs";
import { APIError, betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";
import { verifyBearerToken } from "better-auth/oauth2";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index da2e3787f..9a876c85c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -362,9 +362,9 @@ importers:
ai:
specifier: ^7.0.122
version: 7.0.122(zod@4.6.5)
- bcrypt:
- specifier: ^6.0.0
- version: 6.0.0
+ bcryptjs:
+ specifier: ^3.0.3
+ version: 3.0.3
better-auth:
specifier: 1.7.6
version: 1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@8.0.6)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2)
@@ -887,9 +887,9 @@ importers:
ai:
specifier: ^7.0.122
version: 7.0.122(zod@4.6.5)
- bcrypt:
- specifier: ^6.0.0
- version: 6.0.0
+ bcryptjs:
+ specifier: ^3.0.3
+ version: 3.0.3
better-auth:
specifier: 1.7.6
version: 1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@8.0.6)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2)
@@ -933,9 +933,6 @@ importers:
'@reactive-resume/config':
specifier: workspace:*
version: link:../config
- '@types/bcrypt':
- specifier: ^6.0.0
- version: 6.0.0
'@types/pg':
specifier: ^8.23.1
version: 8.23.1
@@ -984,9 +981,9 @@ importers:
'@reactive-resume/utils':
specifier: workspace:*
version: link:../utils
- bcrypt:
- specifier: ^6.0.0
- version: 6.0.0
+ bcryptjs:
+ specifier: ^3.0.3
+ version: 3.0.3
better-auth:
specifier: 1.7.6
version: 1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@8.0.6)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2)
@@ -1003,9 +1000,6 @@ importers:
'@reactive-resume/config':
specifier: workspace:*
version: link:../config
- '@types/bcrypt':
- specifier: ^6.0.0
- version: 6.0.0
'@types/react':
specifier: ^19.3.0
version: 19.3.0
@@ -4816,9 +4810,6 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
- '@types/bcrypt@6.0.0':
- resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
-
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -5467,9 +5458,9 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
- bcrypt@6.0.0:
- resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
- engines: {node: '>= 18'}
+ bcryptjs@3.0.3:
+ resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==}
+ hasBin: true
better-auth@1.7.6:
resolution: {integrity: sha512-WTMqOpmTTj+oBoX7zzPOzL85342Upyf+/v0IkH1kvGRA85lV4JGRFIYMIRKqvjZ6ShsuL5VX/c9C13jtoZXcKA==}
@@ -7372,18 +7363,10 @@ packages:
sass:
optional: true
- node-addon-api@8.9.2:
- resolution: {integrity: sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==}
- engines: {node: ^18 || ^20 || >= 21}
-
node-forge@1.4.0:
resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==}
engines: {node: '>= 6.13.0'}
- node-gyp-build@4.8.4:
- resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
- hasBin: true
-
node-html-parser@9.0.4:
resolution: {integrity: sha512-reUSrZkF1rO7hEYe5mJwtbVcQ6mB5QrViCZH6Wv+iz3er/PA9Q50gol15dMLQRDfdli4VzqPMPR24HNRFgVKFw==}
@@ -12227,10 +12210,6 @@ snapshots:
dependencies:
'@babel/types': 7.29.8
- '@types/bcrypt@6.0.0':
- dependencies:
- '@types/node': 26.6.3
-
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -12731,10 +12710,7 @@ snapshots:
baseline-browser-mapping@2.11.26: {}
- bcrypt@6.0.0:
- dependencies:
- node-addon-api: 8.9.2
- node-gyp-build: 4.8.4
+ bcryptjs@3.0.3: {}
better-auth@1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7(supports-color@7.2.0))(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2):
dependencies:
@@ -14646,12 +14622,8 @@ snapshots:
- '@types/node'
- babel-plugin-macros
- node-addon-api@8.9.2: {}
-
node-forge@1.4.0: {}
- node-gyp-build@4.8.4: {}
-
node-html-parser@9.0.4:
dependencies:
css-select: 5.2.2
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 6709c1e6d..559ce6c52 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -3,7 +3,6 @@ packages:
- packages/*
- tooling
allowBuilds:
- bcrypt: true
esbuild: true
lefthook: true
sharp: true
diff --git a/vercel.json b/vercel.json
index 872cd014b..3d2d28692 100644
--- a/vercel.json
+++ b/vercel.json
@@ -1,33 +1,36 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
- "framework": null,
- "buildCommand": "pnpm build && node apps/server/dist/prepare-deployment.mjs",
- "outputDirectory": "apps/web/dist",
- "functions": {
- "api/index.mjs": {
- "maxDuration": 300,
- "includeFiles": "{apps/web/dist/index.html,apps/web/dist-prerender/**,apps/server/dist/**,node_modules/.pnpm/pdfkit@*/node_modules/pdfkit/js/{standard-fonts,data}/**}"
+ "services": {
+ "frontend": {
+ "root": "apps/web",
+ "framework": "vite",
+ "buildCommand": "cd ../.. && pnpm build --filter=web",
+ "outputDirectory": "dist"
+ },
+ "backend": {
+ "root": "apps/server",
+ "runtime": "node",
+ "entrypoint": "vercel.mjs",
+ "buildCommand": "cd ../.. && pnpm build --filter=web --filter=server && node apps/server/dist/prepare-deployment.mjs",
+ "outputDirectory": ".",
+ "functions": {
+ "vercel.mjs": {
+ "maxDuration": 300,
+ "includeFiles": "{apps/web/dist/index.html,apps/web/dist-prerender/**,apps/server/dist/prompts/**}"
+ }
+ }
}
},
- "routes": [
+ "rewrites": [
+ { "source": "/api/(.*)", "destination": { "service": "backend" } },
+ { "source": "/uploads/(.*)", "destination": { "service": "backend" } },
+ { "source": "/mcp(/.*)?", "destination": { "service": "backend" } },
+ { "source": "/\\.well-known(/.*)?", "destination": { "service": "backend" } },
{
- "src": "/",
- "dest": "/api/index"
+ "source": "/(index\\.html|robots\\.txt|sitemap\\.xml|llms\\.txt|schema\\.json)",
+ "destination": { "service": "backend" }
},
- {
- "src": "/index.html",
- "dest": "/api/index"
- },
- {
- "src": "/(robots\\.txt|sitemap\\.xml|llms\\.txt|schema\\.json)",
- "dest": "/api/index"
- },
- {
- "handle": "filesystem"
- },
- {
- "src": "/(.*)",
- "dest": "/api/index"
- }
+ { "source": "/(.*\\.[^/]+)", "destination": { "service": "frontend" } },
+ { "source": "/(.*)", "destination": { "service": "backend" } }
]
}