mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-27 10:24:48 +10:00
62f8270b3e
commit b2b0470a1d9267d042ec0ac66523c6635bf5b199
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 13:13:38 2026 +0200
chore: update .gitignore to include .vite-hooks and modify pnpm-lock.yaml for dependencies
commit d28fadb5cd8706c874e616102878b4a394ec84c1
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 13:08:04 2026 +0200
fix: remove timestamp conflict guard
commit c6998d9dbab19d09d3c8054feef1d2e4117555eb
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 12:11:51 2026 +0200
chore(release): v5.1.5
commit f33d168711804880e1f12e88d24290aae16cc258
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 11:58:35 2026 +0200
revert: compose.yml
commit d961e6535811a10c335525fb33a08d03e737278d
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 11:58:08 2026 +0200
refactor(agent): replace 'revert' terminology with 'restore' for clarity, resolves #3086
commit 17f351171be218e33f01c469d95e4164d4c8dc57
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 11:10:41 2026 +0200
refactor(pdf): simplify sidebar section filtering and update summary feature logic
commit d55179b9d76879e3204de185e8b53fadd0a107ed
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:53:37 2026 +0200
chore: update pnpm-lock.yaml and turbo.json
commit 7cade6980e1a04352536bd44ef773f338c4ef599
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:38:30 2026 +0200
fix(polyfill): add tested polyfill for Map Upsert methods
commit 26d175bb9c53d93225d1e907678445252c13d660
Merge: 1cf33dc6c 5b1297fa2
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:23:29 2026 +0200
Merge remote-tracking branch 'origin/main' into feat/explore-hono-orpc-migration
# Conflicts:
# packages/api/src/services/agent-url.ts
# packages/runtime-externals/package.json
commit 1cf33dc6c9d81735730ad656e16dab6501c6d6a1
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Tue May 19 09:22:12 2026 +0200
chore: preserve branch changes before main sync
commit b380a4b00fdbcdd81ff4f8ef72b330fd027ccda5
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Mon May 18 07:50:28 2026 +0200
chore: lot of fixes for monorepo migration
commit 8fcf0ec64e1c29572ebaff494338368bfcf75760
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 13:57:17 2026 +0200
chore: update knip version and refine web app routing with new SEO endpoints
commit 234e68086ff15610a93877354c98e2c020364533
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 12:10:06 2026 +0200
refactor(auth): update OAuth routes to include API prefix and remove unused schema endpoint
commit 91c84b9a8496b0ce21d71cae9f8b2a027638c9ac
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:54:29 2026 +0200
chore: update dependencies and enhance PWA metadata in web app
commit 150117d4a5a9dd6cd92c64891aad8cae90f6a7af
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:12:35 2026 +0200
docs: revise manifest-only pwa testing scope
commit 6b939a55661aec9dd8122b184e4b60a5c7325fb5
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:11:33 2026 +0200
docs: add manifest-only pwa design
commit 1422e1fc96c400948b273210a1067251087d15d4
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:05:04 2026 +0200
chore(dev): simplify server proxy config
commit bc2ff5a9f6fda41e6c40333c8f163aa23a6c5e48
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:04:50 2026 +0200
docs: add unsafe oauth redirect plan
commit 445359ebe9b96c1515bf1c4c3f73ba8a8448ec12
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 11:04:34 2026 +0200
feat(auth): add unsafe oauth redirect flag
commit 73fffdd24598e56b2793f7657919bc794835892e
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 10:55:02 2026 +0200
docs: design unsafe oauth redirect flag
commit c0066aa19c15fc8a4c8e5179ed49889c117519f4
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 10:22:04 2026 +0200
chore: update translation source paths
commit 9033da082418d252aafd6c2eed72f71f014be3d9
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 10:09:25 2026 +0200
refactor(arch): react spa + hono migration
commit 6f27936c11bda895977dc63ee550c3346d4ce24b
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Fri May 15 01:10:47 2026 +0200
docs: add docker nightly tagging design
commit ecc1fd9a88a0ee1dca2f1977dfc17f74527fe1da
Author: Amruth Pillai <im.amruth@gmail.com>
Date: Thu May 14 20:05:44 2026 +0200
feat: migrate to hono spa server
1144 lines
33 KiB
Markdown
1144 lines
33 KiB
Markdown
# Monolith and Split Deployment Architecture Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
> **Status note:** This is a historical proposal, not the current architecture source of truth. Before executing any task from this plan, verify paths and route ownership against `AGENTS.md`, `docs/contributing/architecture.mdx`, and the current `apps/server/src/{http,rpc,mcp,openapi,static,startup}` tree.
|
|
|
|
**Goal:** Preserve the existing one-image Docker contract for open-source self-hosters while enabling the same codebase to run as an edge/static frontend plus Node backend for hosted and VPS split deployments.
|
|
|
|
**Architecture:** Reactive Resume should have one route ownership model and multiple deployment compositions. The OSS Docker image remains a monolith: one image, one port, backend routes plus static SPA fallback. Hosted/split deployments serve the built SPA statically from Cloudflare/S3/NGINX/Caddy and route only backend-owned paths to the Node server, where unknown frontend routes return `404`.
|
|
|
|
**Tech Stack:** Hono, Better Auth, Vite, Turborepo, Docker, optional NGINX/Caddy/CDN routing, Zod JSON Schema generation.
|
|
|
|
---
|
|
|
|
## Decisions Locked By This Plan
|
|
|
|
- Keep the official Docker image as a complete monolith.
|
|
- Do not make NGINX a required wrapper for the official OSS image.
|
|
- Use Hono static serving as the Docker/self-host fallback.
|
|
- Prefer edge/static frontend plus Node backend for hosted Reactive Resume.
|
|
- Do not pursue Cloudflare Worker backend support in this phase.
|
|
- Keep backend logic Node-first and platform-independent where practical, but do not contort PDF, database, storage, or auth runtime code for Worker compatibility yet.
|
|
- Move custom OAuth bridge behavior under `/api/auth/oauth`.
|
|
- Keep `/.well-known/*` backend-owned because OAuth/MCP metadata is runtime-owned.
|
|
- Make `/schema.json` frontend/static-owned by generating it during build/source preparation.
|
|
- In split backend mode, unknown non-backend routes return `404`.
|
|
|
|
## Deployment Modes
|
|
|
|
### Mode 1: Docker Monolith
|
|
|
|
**Audience:** OSS users and current Docker image consumers.
|
|
|
|
```txt
|
|
Request -> Node/Hono server
|
|
/api/* -> backend
|
|
/api/auth/oauth -> custom OAuth bridge
|
|
/mcp, /mcp/* -> backend
|
|
/.well-known/* -> backend
|
|
/uploads/* -> backend unless object storage owns this later
|
|
/schema.json -> static file from apps/web/dist
|
|
static assets -> static file from apps/web/dist
|
|
non-file GET/HEAD -> apps/web/dist/index.html
|
|
file-looking miss -> 404
|
|
```
|
|
|
|
### Mode 2: Split VPS
|
|
|
|
**Audience:** Advanced self-hosters or the project owner's VPS migration path.
|
|
|
|
```txt
|
|
Request -> NGINX/Caddy
|
|
static files -> apps/web/dist
|
|
SPA fallback -> apps/web/dist/index.html
|
|
backend routes -> proxy to Node
|
|
|
|
Request -> Node/Hono backend-only app
|
|
backend routes -> backend
|
|
unknown routes -> 404
|
|
```
|
|
|
|
### Mode 3: Hosted Edge Frontend + Node Backend
|
|
|
|
**Audience:** Cloud hosted Reactive Resume.
|
|
|
|
```txt
|
|
Request -> Cloudflare/S3/CDN
|
|
static files -> edge object/static hosting
|
|
SPA fallback -> edge index.html
|
|
backend routes -> proxy/fetch to Node backend
|
|
|
|
Request -> Node backend
|
|
backend routes -> backend
|
|
unknown routes -> 404
|
|
```
|
|
|
|
---
|
|
|
|
## Route Ownership Contract
|
|
|
|
### Backend-Owned Routes
|
|
|
|
```txt
|
|
/api/*
|
|
/api/auth/oauth
|
|
/mcp
|
|
/mcp/*
|
|
/.well-known/*
|
|
/uploads/*
|
|
```
|
|
|
|
`/uploads/*` remains backend-owned while local filesystem uploads are supported. If uploads move fully to S3/R2 public or signed URLs later, this can become object-storage-owned.
|
|
|
|
### Frontend/Static-Owned Routes
|
|
|
|
```txt
|
|
/schema.json
|
|
/assets/*
|
|
/templates/*
|
|
/favicon.ico
|
|
/favicon.svg
|
|
/manifest.webmanifest
|
|
/robots.txt
|
|
/sitemap.xml
|
|
/fonts/*
|
|
/icon/*
|
|
/logo/*
|
|
/opengraph/*
|
|
/photos/*
|
|
/pwa-*.png
|
|
/sounds/*
|
|
/videos/*
|
|
```
|
|
|
|
### Frontend SPA-Owned Routes
|
|
|
|
```txt
|
|
/auth/*
|
|
/dashboard/*
|
|
/builder/*
|
|
/agent/*
|
|
/$username/$slug
|
|
all other non-file GET/HEAD frontend routes
|
|
```
|
|
|
|
In Docker monolith mode, Node serves the SPA fallback. In split modes, the static frontend host serves the SPA fallback and Node returns `404` for unknown non-backend routes.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
- Create `packages/server`
|
|
- Owns route ownership constants and shared Hono app composition.
|
|
- Exposes `createBackendApp()` and `createMonolithApp()`.
|
|
- Modify `apps/server/src/index.ts`
|
|
- Becomes a thin Node entrypoint around `@reactive-resume/server/node`.
|
|
- Selects monolith/backend-only mode through explicit env/config.
|
|
- Modify `apps/server/src/handlers/*`
|
|
- Move or re-export handlers into `packages/server` only after package boundaries are clear.
|
|
- Keep this as a later task if direct movement is too large.
|
|
- Modify `packages/auth/src/config.ts`
|
|
- Change OAuth provider `loginPage` and `consentPage` to `/api/auth/oauth`.
|
|
- Modify `apps/server/src/handlers/auth.ts`
|
|
- Change callback preservation to `/api/auth/oauth`.
|
|
- Create `packages/scripts/schema/generate.ts`
|
|
- Generates both package and web-public schema artifacts.
|
|
- Create `apps/web/public/schema.json`
|
|
- Static schema artifact for Vite, Docker monolith, S3, and edge hosting.
|
|
- Modify `apps/web/package.json`
|
|
- Runs schema generation before build.
|
|
- Modify `apps/web/vite.config.ts`
|
|
- Remove dev proxies that become unnecessary after route/static ownership cleanup.
|
|
- Modify `Dockerfile`
|
|
- Preserve one-image behavior.
|
|
- Make runtime default to monolith mode.
|
|
- Create `docs/deployment/architecture.mdx`
|
|
- Explains monolith vs split modes.
|
|
- Create `docs/deployment/split-vps.mdx`
|
|
- Shows NGINX/Caddy examples.
|
|
- Create `docs/deployment/edge-frontend-node-backend.mdx`
|
|
- Shows Cloudflare/S3/CDN static frontend plus Node backend.
|
|
- Create or update `AGENTS.md`
|
|
- Records route ownership and deployment-mode rules as normative architecture guidance.
|
|
|
|
---
|
|
|
|
## Task 1: Document The Deployment Contract First
|
|
|
|
**Files:**
|
|
- Create: `docs/deployment/architecture.mdx`
|
|
- Create: `docs/deployment/split-vps.mdx`
|
|
- Create: `docs/deployment/edge-frontend-node-backend.mdx`
|
|
- Modify: `AGENTS.md`
|
|
|
|
- [ ] **Step 1: Write `docs/deployment/architecture.mdx`**
|
|
|
|
Create the document with these sections:
|
|
|
|
```mdx
|
|
# Deployment Architecture
|
|
|
|
Reactive Resume supports multiple deployment compositions from one codebase.
|
|
|
|
## Default: Docker Monolith
|
|
|
|
The official Docker image remains a complete application image. It exposes one port and serves backend routes, static assets, and SPA fallback from the same Node process.
|
|
|
|
This is the compatibility contract for open-source self-hosters.
|
|
|
|
## Split Frontend And Backend
|
|
|
|
Split deployments serve `apps/web/dist` from a static host and route backend-owned paths to the Node server.
|
|
|
|
The Node server does not serve SPA fallback in backend-only mode. Unknown non-backend routes return `404`.
|
|
|
|
## Route Ownership
|
|
|
|
Backend-owned:
|
|
|
|
- `/api/*`
|
|
- `/api/auth/oauth`
|
|
- `/mcp`
|
|
- `/mcp/*`
|
|
- `/.well-known/*`
|
|
- `/uploads/*`
|
|
|
|
Frontend/static-owned:
|
|
|
|
- `/schema.json`
|
|
- `/assets/*`
|
|
- `/templates/*`
|
|
- `/favicon.ico`
|
|
- `/favicon.svg`
|
|
- `/manifest.webmanifest`
|
|
- `/robots.txt`
|
|
- `/sitemap.xml`
|
|
- `/fonts/*`
|
|
- `/icon/*`
|
|
- `/logo/*`
|
|
- `/opengraph/*`
|
|
- `/photos/*`
|
|
- `/pwa-*.png`
|
|
- `/sounds/*`
|
|
- `/videos/*`
|
|
|
|
Frontend SPA-owned:
|
|
|
|
- `/auth/*`
|
|
- `/dashboard/*`
|
|
- `/builder/*`
|
|
- `/agent/*`
|
|
- public resume pages
|
|
- all other non-file frontend routes
|
|
|
|
## Runtime Modes
|
|
|
|
- `monolith`: backend routes plus static SPA serving.
|
|
- `backend-only`: backend routes only; unknown routes return `404`.
|
|
```
|
|
|
|
- [ ] **Step 2: Write `docs/deployment/split-vps.mdx`**
|
|
|
|
Include this minimal NGINX shape:
|
|
|
|
```nginx
|
|
server {
|
|
listen 80;
|
|
server_name example.com;
|
|
|
|
root /srv/reactive-resume/web;
|
|
index index.html;
|
|
|
|
location ^~ /api/ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
}
|
|
|
|
location = /mcp {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
}
|
|
|
|
location ^~ /mcp/ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
}
|
|
|
|
location ^~ /.well-known/ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
}
|
|
|
|
location ^~ /uploads/ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
}
|
|
|
|
location / {
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
}
|
|
```
|
|
|
|
Add a note that `APP_URL` must be the public origin.
|
|
|
|
- [ ] **Step 3: Write `docs/deployment/edge-frontend-node-backend.mdx`**
|
|
|
|
Include:
|
|
|
|
```mdx
|
|
# Edge Frontend With Node Backend
|
|
|
|
Build `apps/web/dist` and upload it to the static host. Route backend-owned paths to the Node backend.
|
|
|
|
Backend route patterns:
|
|
|
|
- `/api/*`
|
|
- `/mcp`
|
|
- `/mcp/*`
|
|
- `/.well-known/*`
|
|
- `/uploads/*`
|
|
|
|
The frontend host owns `/schema.json` and SPA fallback.
|
|
|
|
The Node backend should run in backend-only mode so unknown frontend routes return `404`.
|
|
```
|
|
|
|
- [ ] **Step 4: Update `AGENTS.md`**
|
|
|
|
Add a short normative section:
|
|
|
|
```md
|
|
### Deployment route ownership
|
|
|
|
The official Docker image is a monolith and must keep serving the full application from one image and one port.
|
|
|
|
Split deployments are supported by serving `apps/web/dist` statically and routing backend-owned paths to Node. In backend-only mode, Node returns `404` for unknown frontend routes.
|
|
|
|
Do not add new backend routes outside `/api/*`, `/mcp`, `/.well-known/*`, or explicitly documented exceptions without updating the route ownership docs.
|
|
```
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add docs/deployment/architecture.mdx docs/deployment/split-vps.mdx docs/deployment/edge-frontend-node-backend.mdx AGENTS.md
|
|
git commit -m "docs: define monolith and split deployment contract"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2: Create Route Ownership Primitives
|
|
|
|
**Files:**
|
|
- Create: `packages/server/package.json`
|
|
- Create: `packages/server/tsconfig.json`
|
|
- Create: `packages/server/vitest.config.ts`
|
|
- Create: `packages/server/src/routes/ownership.ts`
|
|
- Create: `packages/server/src/routes/ownership.test.ts`
|
|
|
|
- [ ] **Step 1: Write the failing ownership tests**
|
|
|
|
Create `packages/server/src/routes/ownership.test.ts`:
|
|
|
|
```ts
|
|
import { describe, expect, it } from "vitest";
|
|
import { getRouteOwner } from "./ownership";
|
|
|
|
describe("getRouteOwner", () => {
|
|
it.each([
|
|
["/api/rpc", "backend"],
|
|
["/api/auth/oauth", "backend"],
|
|
["/mcp", "backend"],
|
|
["/mcp/tools", "backend"],
|
|
["/.well-known/oauth-protected-resource", "backend"],
|
|
["/uploads/avatar.png", "backend"],
|
|
["/schema.json", "static"],
|
|
["/assets/app.js", "static"],
|
|
["/templates/jpg/azurill.jpg", "static"],
|
|
["/auth/login", "spa"],
|
|
["/dashboard", "spa"],
|
|
["/builder/abc", "spa"],
|
|
["/agent/thread", "spa"],
|
|
["/amruth/resume", "spa"],
|
|
])("classifies %s as %s", (pathname, owner) => {
|
|
expect(getRouteOwner(pathname)).toBe(owner);
|
|
});
|
|
|
|
it.each(["/missing.js", "/unknown.css", "/image.png"])("classifies file-looking misses as static", (pathname) => {
|
|
expect(getRouteOwner(pathname)).toBe("static");
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Add package scaffolding**
|
|
|
|
Create `packages/server/package.json`:
|
|
|
|
```json
|
|
{
|
|
"name": "@reactive-resume/server",
|
|
"version": "0.0.0",
|
|
"type": "module",
|
|
"private": true,
|
|
"exports": {
|
|
"./routes/ownership": "./src/routes/ownership.ts"
|
|
},
|
|
"scripts": {
|
|
"typecheck": "tsgo --noEmit",
|
|
"test": "vitest run --passWithNoTests"
|
|
},
|
|
"devDependencies": {
|
|
"@reactive-resume/config": "workspace:*",
|
|
"@typescript/native-preview": "7.0.0-dev.20260514.1",
|
|
"typescript": "^6.0.3"
|
|
}
|
|
}
|
|
```
|
|
|
|
Create `packages/server/tsconfig.json`:
|
|
|
|
```json
|
|
{
|
|
"extends": "@reactive-resume/config/tsconfig.base.json"
|
|
}
|
|
```
|
|
|
|
Create `packages/server/vitest.config.ts`:
|
|
|
|
```ts
|
|
import { createVitestProjectConfig } from "@reactive-resume/config/vitest";
|
|
|
|
export default createVitestProjectConfig({
|
|
name: "@reactive-resume/server",
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 3: Run the failing tests**
|
|
|
|
```bash
|
|
pnpm --filter @reactive-resume/server test -- src/routes/ownership.test.ts
|
|
```
|
|
|
|
Expected: fails because `ownership.ts` does not exist.
|
|
|
|
- [ ] **Step 4: Implement route ownership**
|
|
|
|
Create `packages/server/src/routes/ownership.ts`:
|
|
|
|
```ts
|
|
export type RouteOwner = "backend" | "static" | "spa";
|
|
|
|
const backendExactRoutes = new Set(["/mcp"]);
|
|
const backendPrefixes = ["/api/", "/mcp/", "/.well-known/", "/uploads/"];
|
|
const staticExactRoutes = new Set([
|
|
"/schema.json",
|
|
"/favicon.ico",
|
|
"/favicon.svg",
|
|
"/manifest.webmanifest",
|
|
"/robots.txt",
|
|
"/sitemap.xml",
|
|
]);
|
|
const staticPrefixes = [
|
|
"/assets/",
|
|
"/templates/",
|
|
"/fonts/",
|
|
"/icon/",
|
|
"/logo/",
|
|
"/opengraph/",
|
|
"/photos/",
|
|
"/sounds/",
|
|
"/videos/",
|
|
];
|
|
|
|
function looksLikeFile(pathname: string) {
|
|
const lastSegment = pathname.split("/").pop() ?? "";
|
|
return lastSegment.includes(".");
|
|
}
|
|
|
|
export function getRouteOwner(pathname: string): RouteOwner {
|
|
if (backendExactRoutes.has(pathname)) return "backend";
|
|
if (backendPrefixes.some((prefix) => pathname.startsWith(prefix))) return "backend";
|
|
if (staticExactRoutes.has(pathname)) return "static";
|
|
if (staticPrefixes.some((prefix) => pathname.startsWith(prefix))) return "static";
|
|
if (/^\/pwa-\d+x\d+\.png$/.test(pathname)) return "static";
|
|
if (looksLikeFile(pathname)) return "static";
|
|
return "spa";
|
|
}
|
|
|
|
export function isBackendRoute(pathname: string) {
|
|
return getRouteOwner(pathname) === "backend";
|
|
}
|
|
|
|
export function isStaticRoute(pathname: string) {
|
|
return getRouteOwner(pathname) === "static";
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Verify route ownership tests**
|
|
|
|
```bash
|
|
pnpm --filter @reactive-resume/server test -- src/routes/ownership.test.ts
|
|
pnpm --filter @reactive-resume/server typecheck
|
|
```
|
|
|
|
Expected: both pass.
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add packages/server
|
|
git commit -m "feat(server): add route ownership contract"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 3: Move Custom OAuth Bridge To `/api/auth/oauth`
|
|
|
|
**Files:**
|
|
- Modify: `apps/server/src/handlers/auth.test.ts`
|
|
- Create: `apps/server/src/index.test.ts`
|
|
- Modify: `apps/server/src/index.ts`
|
|
- Modify: `apps/server/src/handlers/auth.ts`
|
|
- Modify: `packages/auth/src/config.ts`
|
|
- Modify: `apps/web/vite.config.ts`
|
|
|
|
- [ ] **Step 1: Update the handler test first**
|
|
|
|
In `apps/server/src/handlers/auth.test.ts`, change the request URL and expected callback path:
|
|
|
|
```ts
|
|
const response = await handleOAuth(
|
|
new Request(
|
|
"http://localhost:3001/api/auth/oauth?client_id=test-client&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&state=abc&exp=123&sig=456",
|
|
),
|
|
);
|
|
|
|
expect(callbackUrl.pathname).toBe("/api/auth/oauth");
|
|
```
|
|
|
|
- [ ] **Step 2: Add a route precedence test**
|
|
|
|
Create `apps/server/src/index.test.ts`:
|
|
|
|
```ts
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
handleAuth: vi.fn(),
|
|
handleOAuth: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("./handlers/auth", () => ({
|
|
handleAuth: mocks.handleAuth,
|
|
handleOAuth: mocks.handleOAuth,
|
|
}));
|
|
|
|
vi.mock("./handlers/health", () => ({ handleHealth: () => Response.json({ ok: true }) }));
|
|
vi.mock("./handlers/mcp", () => ({ handleMcp: () => new Response("mcp") }));
|
|
vi.mock("./handlers/metadata", () => ({
|
|
handleMcpServerCard: () => Response.json({}),
|
|
handleOAuthAuthorizationServer: () => Response.json({}),
|
|
handleOAuthProtectedResource: () => Response.json({}),
|
|
handleOpenIdConfiguration: () => Response.json({}),
|
|
handleWellKnownFallback: () => new Response("OK"),
|
|
}));
|
|
vi.mock("./handlers/openapi", () => ({ handleOpenApi: () => Response.json({}) }));
|
|
vi.mock("./handlers/rpc", () => ({ handleRpc: () => Response.json({}) }));
|
|
vi.mock("./handlers/uploads", () => ({ handleUpload: () => new Response("upload") }));
|
|
|
|
describe("createApp route ownership", () => {
|
|
beforeEach(() => {
|
|
mocks.handleAuth.mockReset();
|
|
mocks.handleOAuth.mockReset();
|
|
mocks.handleAuth.mockResolvedValue(Response.json({ route: "auth" }));
|
|
mocks.handleOAuth.mockResolvedValue(Response.json({ route: "oauth" }));
|
|
});
|
|
|
|
it("routes /api/auth/oauth to the custom OAuth bridge before the Better Auth catch-all", async () => {
|
|
const { createApp } = await import("./index");
|
|
const response = await createApp().request("http://localhost:3001/api/auth/oauth?client_id=test");
|
|
|
|
await expect(response.json()).resolves.toEqual({ route: "oauth" });
|
|
expect(mocks.handleOAuth).toHaveBeenCalledTimes(1);
|
|
expect(mocks.handleAuth).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps other /api/auth/* requests on the Better Auth handler", async () => {
|
|
const { createApp } = await import("./index");
|
|
const response = await createApp().request("http://localhost:3001/api/auth/session");
|
|
|
|
await expect(response.json()).resolves.toEqual({ route: "auth" });
|
|
expect(mocks.handleAuth).toHaveBeenCalledTimes(1);
|
|
expect(mocks.handleOAuth).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 3: Run failing tests**
|
|
|
|
```bash
|
|
pnpm --filter server test -- src/handlers/auth.test.ts src/index.test.ts
|
|
```
|
|
|
|
Expected: tests fail because the implementation still uses `/auth/oauth`.
|
|
|
|
- [ ] **Step 4: Update server route order**
|
|
|
|
In `apps/server/src/index.ts`, register the custom route before Better Auth catch-all:
|
|
|
|
```ts
|
|
app.get("/api/auth/oauth", (c) => handleOAuth(c.req.raw));
|
|
app.on(["GET", "POST"], "/api/auth/*", (c) => handleAuth(c.req.raw));
|
|
```
|
|
|
|
Remove:
|
|
|
|
```ts
|
|
app.get("/auth/oauth", (c) => handleOAuth(c.req.raw));
|
|
```
|
|
|
|
- [ ] **Step 5: Update callback preservation**
|
|
|
|
In `apps/server/src/handlers/auth.ts`, change:
|
|
|
|
```ts
|
|
loginUrl.searchParams.set("callbackURL", `/auth/oauth?${oauthParams.toString()}`);
|
|
```
|
|
|
|
to:
|
|
|
|
```ts
|
|
loginUrl.searchParams.set("callbackURL", `/api/auth/oauth?${oauthParams.toString()}`);
|
|
```
|
|
|
|
- [ ] **Step 6: Update Better Auth OAuth provider pages**
|
|
|
|
In `packages/auth/src/config.ts`, change:
|
|
|
|
```ts
|
|
oauthProvider({
|
|
loginPage: "/api/auth/oauth",
|
|
consentPage: "/api/auth/oauth",
|
|
```
|
|
|
|
- [ ] **Step 7: Remove old dev proxy**
|
|
|
|
In `apps/web/vite.config.ts`, remove the `"/auth/oauth"` proxy entry. `/api/auth/oauth` is covered by the existing `"/api"` proxy.
|
|
|
|
- [ ] **Step 8: Verify and commit**
|
|
|
|
```bash
|
|
pnpm --filter server test -- src/handlers/auth.test.ts src/index.test.ts
|
|
pnpm --filter server typecheck
|
|
pnpm --filter @reactive-resume/auth typecheck
|
|
```
|
|
|
|
Expected: all pass.
|
|
|
|
```bash
|
|
git add apps/server/src/handlers/auth.test.ts apps/server/src/index.test.ts apps/server/src/index.ts apps/server/src/handlers/auth.ts packages/auth/src/config.ts apps/web/vite.config.ts
|
|
git commit -m "fix(auth): move OAuth bridge under api auth routes"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4: Make `/schema.json` Static
|
|
|
|
**Files:**
|
|
- Create: `packages/scripts/schema/generate.ts`
|
|
- Create: `packages/scripts/schema/generate.test.ts`
|
|
- Modify: `packages/scripts/package.json`
|
|
- Modify: `apps/web/package.json`
|
|
- Create: `apps/web/public/schema.json`
|
|
- Modify: `apps/web/vite.config.ts`
|
|
- Modify: `apps/server/src/index.ts`
|
|
- Modify: `apps/server/src/handlers/metadata.ts`
|
|
|
|
- [ ] **Step 1: Write generator test first**
|
|
|
|
Create `packages/scripts/schema/generate.test.ts`:
|
|
|
|
```ts
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { generateResumeSchemaJson } from "./generate";
|
|
|
|
describe("generateResumeSchemaJson", () => {
|
|
it("writes identical resume JSON Schema to every target", async () => {
|
|
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "resume-schema-"));
|
|
const firstTarget = path.join(directory, "schema-a.json");
|
|
const secondTarget = path.join(directory, "nested", "schema-b.json");
|
|
|
|
await generateResumeSchemaJson([firstTarget, secondTarget]);
|
|
|
|
const first = await fs.readFile(firstTarget, "utf-8");
|
|
const second = await fs.readFile(secondTarget, "utf-8");
|
|
|
|
expect(JSON.parse(first)).toEqual(JSON.parse(second));
|
|
expect(JSON.parse(first)).toHaveProperty("properties.basics");
|
|
expect(first.endsWith("\n")).toBe(true);
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Add script package entries**
|
|
|
|
In `packages/scripts/package.json`, add:
|
|
|
|
```json
|
|
"schema:generate": "tsx schema/generate.ts",
|
|
"test": "vitest run --passWithNoTests"
|
|
```
|
|
|
|
Add missing dev dependencies:
|
|
|
|
```json
|
|
"@reactive-resume/schema": "workspace:*",
|
|
"vitest": "^4.1.6",
|
|
"zod": "^4.4.3"
|
|
```
|
|
|
|
- [ ] **Step 3: Run failing generator test**
|
|
|
|
```bash
|
|
pnpm --filter @reactive-resume/scripts test -- schema/generate.test.ts
|
|
```
|
|
|
|
Expected: fails because `generate.ts` does not exist.
|
|
|
|
- [ ] **Step 4: Implement generator**
|
|
|
|
Create `packages/scripts/schema/generate.ts`:
|
|
|
|
```ts
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import z from "zod";
|
|
import { resumeDataSchema } from "@reactive-resume/schema/resume/data";
|
|
|
|
const defaultTargets = [
|
|
new URL("../../schema/schema.json", import.meta.url),
|
|
new URL("../../../apps/web/public/schema.json", import.meta.url),
|
|
];
|
|
|
|
function toPath(target: string | URL) {
|
|
return target instanceof URL ? fileURLToPath(target) : target;
|
|
}
|
|
|
|
export async function generateResumeSchemaJson(targets: Array<string | URL> = defaultTargets) {
|
|
const schema = z.toJSONSchema(resumeDataSchema);
|
|
const contents = `${JSON.stringify(schema, null, "\t")}\n`;
|
|
|
|
await Promise.all(
|
|
targets.map(async (target) => {
|
|
const targetPath = toPath(target);
|
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
await fs.writeFile(targetPath, contents);
|
|
}),
|
|
);
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
await generateResumeSchemaJson();
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Wire generation into web build**
|
|
|
|
In `apps/web/package.json`, add:
|
|
|
|
```json
|
|
"prebuild": "pnpm --filter @reactive-resume/scripts schema:generate"
|
|
```
|
|
|
|
Keep:
|
|
|
|
```json
|
|
"build": "rm -rf dist && vite build"
|
|
```
|
|
|
|
- [ ] **Step 6: Remove backend runtime schema route**
|
|
|
|
In `apps/server/src/index.ts`, remove the `handleSchemaJson` import and:
|
|
|
|
```ts
|
|
app.get("/schema.json", () => handleSchemaJson());
|
|
```
|
|
|
|
In `apps/server/src/handlers/metadata.ts`, remove:
|
|
|
|
```ts
|
|
import z from "zod";
|
|
import { resumeDataSchema } from "@reactive-resume/schema/resume/data";
|
|
```
|
|
|
|
and delete `handleSchemaJson()`.
|
|
|
|
- [ ] **Step 7: Remove web dev proxy for schema**
|
|
|
|
In `apps/web/vite.config.ts`, remove the `"/schema.json"` proxy entry.
|
|
|
|
- [ ] **Step 8: Generate and verify**
|
|
|
|
```bash
|
|
pnpm --filter @reactive-resume/scripts schema:generate
|
|
pnpm --filter @reactive-resume/scripts test -- schema/generate.test.ts
|
|
pnpm --filter @reactive-resume/scripts typecheck
|
|
pnpm --filter server typecheck
|
|
pnpm --filter web build
|
|
test -f apps/web/dist/schema.json
|
|
cmp packages/schema/schema.json apps/web/public/schema.json
|
|
```
|
|
|
|
Expected: all commands pass.
|
|
|
|
- [ ] **Step 9: Commit**
|
|
|
|
```bash
|
|
git add packages/scripts/schema packages/scripts/package.json apps/web/package.json apps/web/public/schema.json apps/web/vite.config.ts apps/server/src/index.ts apps/server/src/handlers/metadata.ts packages/schema/schema.json
|
|
git commit -m "feat(web): serve resume schema as static asset"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 5: Split Server App Composition Into Backend-Only And Monolith
|
|
|
|
**Files:**
|
|
- Create: `apps/server/src/app.ts`
|
|
- Create: `apps/server/src/app.test.ts`
|
|
- Modify: `apps/server/src/index.ts`
|
|
- Modify: `apps/server/package.json`
|
|
|
|
- [ ] **Step 1: Add tests for deployment mode behavior**
|
|
|
|
Create `apps/server/src/app.test.ts`:
|
|
|
|
```ts
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("./handlers/auth", () => ({
|
|
handleAuth: () => Response.json({ route: "auth" }),
|
|
handleOAuth: () => Response.json({ route: "oauth" }),
|
|
}));
|
|
vi.mock("./handlers/health", () => ({ handleHealth: () => Response.json({ ok: true }) }));
|
|
vi.mock("./handlers/mcp", () => ({ handleMcp: () => new Response("mcp") }));
|
|
vi.mock("./handlers/metadata", () => ({
|
|
handleMcpServerCard: () => Response.json({}),
|
|
handleOAuthAuthorizationServer: () => Response.json({}),
|
|
handleOAuthProtectedResource: () => Response.json({}),
|
|
handleOpenIdConfiguration: () => Response.json({}),
|
|
handleWellKnownFallback: () => new Response("OK"),
|
|
}));
|
|
vi.mock("./handlers/openapi", () => ({ handleOpenApi: () => Response.json({}) }));
|
|
vi.mock("./handlers/rpc", () => ({ handleRpc: () => Response.json({}) }));
|
|
vi.mock("./handlers/uploads", () => ({ handleUpload: () => new Response("upload") }));
|
|
|
|
describe("server app deployment modes", () => {
|
|
it("returns 404 for frontend routes in backend-only mode", async () => {
|
|
const { createBackendApp } = await import("./app");
|
|
const response = await createBackendApp().request("http://localhost:3001/dashboard");
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it("keeps backend routes available in backend-only mode", async () => {
|
|
const { createBackendApp } = await import("./app");
|
|
const response = await createBackendApp().request("http://localhost:3001/api/auth/oauth");
|
|
|
|
expect(response.status).toBe(200);
|
|
await expect(response.json()).resolves.toEqual({ route: "oauth" });
|
|
});
|
|
});
|
|
```
|
|
|
|
- [ ] **Step 2: Run failing tests**
|
|
|
|
```bash
|
|
pnpm --filter server test -- src/app.test.ts
|
|
```
|
|
|
|
Expected: fails because `app.ts` does not exist.
|
|
|
|
- [ ] **Step 3: Extract backend app composition**
|
|
|
|
Create `apps/server/src/app.ts`:
|
|
|
|
```ts
|
|
import fs from "node:fs/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
import { Hono } from "hono";
|
|
import { handleAuth, handleOAuth } from "./handlers/auth";
|
|
import { handleHealth } from "./handlers/health";
|
|
import { handleMcp } from "./handlers/mcp";
|
|
import {
|
|
handleMcpServerCard,
|
|
handleOAuthAuthorizationServer,
|
|
handleOAuthProtectedResource,
|
|
handleOpenIdConfiguration,
|
|
handleWellKnownFallback,
|
|
} from "./handlers/metadata";
|
|
import { handleOpenApi } from "./handlers/openapi";
|
|
import { handleRpc } from "./handlers/rpc";
|
|
import { handleUpload } from "./handlers/uploads";
|
|
|
|
const staticRoot = fileURLToPath(new URL("../../web/dist", import.meta.url));
|
|
const indexHtmlPath = fileURLToPath(new URL("../../web/dist/index.html", import.meta.url));
|
|
|
|
function registerBackendRoutes(app: Hono) {
|
|
app.all("/api/rpc", (c) => handleRpc(c.req.raw));
|
|
app.all("/api/rpc/*", (c) => handleRpc(c.req.raw));
|
|
app.all("/api/openapi", (c) => handleOpenApi(c.req.raw));
|
|
app.all("/api/openapi/*", (c) => handleOpenApi(c.req.raw));
|
|
app.get("/api/auth/oauth", (c) => handleOAuth(c.req.raw));
|
|
app.on(["GET", "POST"], "/api/auth/*", (c) => handleAuth(c.req.raw));
|
|
app.get("/api/health", () => handleHealth());
|
|
app.get("/api/uploads/*", (c) => handleUpload(c.req.raw));
|
|
app.get("/uploads/*", (c) => handleUpload(c.req.raw));
|
|
app.all("/mcp", (c) => handleMcp(c.req.raw));
|
|
app.all("/mcp/*", (c) => handleMcp(c.req.raw));
|
|
app.get("/.well-known/mcp/server-card.json", () => handleMcpServerCard());
|
|
app.get("/.well-known/oauth-authorization-server", (c) => handleOAuthAuthorizationServer(c.req.raw));
|
|
app.get("/.well-known/oauth-authorization-server/*", (c) => handleOAuthAuthorizationServer(c.req.raw));
|
|
app.get("/.well-known/openid-configuration", (c) => handleOpenIdConfiguration(c.req.raw));
|
|
app.get("/.well-known/oauth-protected-resource", () => handleOAuthProtectedResource());
|
|
app.get("/.well-known/oauth-protected-resource/*", () => handleOAuthProtectedResource());
|
|
app.get("/.well-known/*", () => handleWellKnownFallback());
|
|
app.on(["HEAD"], "/.well-known/*", () => handleWellKnownFallback());
|
|
}
|
|
|
|
export function createBackendApp() {
|
|
const app = new Hono();
|
|
registerBackendRoutes(app);
|
|
app.all("/*", () => new Response("Not Found", { status: 404 }));
|
|
return app;
|
|
}
|
|
|
|
export function createMonolithApp() {
|
|
const app = new Hono();
|
|
registerBackendRoutes(app);
|
|
|
|
app.use("/*", serveStatic({ root: staticRoot, precompressed: true }));
|
|
app.get("/*", async (c) => {
|
|
const pathname = new URL(c.req.url).pathname;
|
|
if (pathname.split("/").pop()?.includes(".")) return c.text("Not Found", 404);
|
|
|
|
const html = await fs.readFile(indexHtmlPath, "utf-8");
|
|
return c.html(html);
|
|
});
|
|
app.on(["HEAD"], "/*", async (c) => {
|
|
const pathname = new URL(c.req.url).pathname;
|
|
if (pathname.split("/").pop()?.includes(".")) return c.body(null, 404);
|
|
|
|
return c.body(null, 200, { "Content-Type": "text/html; charset=UTF-8" });
|
|
});
|
|
|
|
return app;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Make index choose mode**
|
|
|
|
In `apps/server/src/index.ts`, replace inline app creation with:
|
|
|
|
```ts
|
|
import { pathToFileURL } from "node:url";
|
|
import { serve } from "@hono/node-server";
|
|
import { env } from "@reactive-resume/env/server";
|
|
import { createBackendApp, createMonolithApp } from "./app";
|
|
import { runStartupChecks } from "./lib/startup";
|
|
|
|
export function createApp() {
|
|
return process.env.SERVER_MODE === "backend-only" ? createBackendApp() : createMonolithApp();
|
|
}
|
|
|
|
async function main() {
|
|
await runStartupChecks();
|
|
|
|
const port =
|
|
process.env.NODE_ENV === "production" ? Number.parseInt(process.env.PORT ?? "3000", 10) : env.SERVER_PORT;
|
|
const app = createApp();
|
|
|
|
serve(
|
|
{
|
|
fetch: app.fetch,
|
|
port,
|
|
},
|
|
(info) => {
|
|
console.info(`🚀 Up and running on http://localhost:${info.port}`);
|
|
},
|
|
);
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
await main();
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Verify**
|
|
|
|
```bash
|
|
pnpm --filter server test -- src/app.test.ts src/index.test.ts
|
|
pnpm --filter server typecheck
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
- [ ] **Step 6: Commit**
|
|
|
|
```bash
|
|
git add apps/server/src/app.ts apps/server/src/app.test.ts apps/server/src/index.ts apps/server/package.json
|
|
git commit -m "feat(server): split backend and monolith app modes"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 6: Preserve Docker Monolith Contract
|
|
|
|
**Files:**
|
|
- Modify: `Dockerfile`
|
|
- Modify: `compose.yml`
|
|
- Modify: `compose.dev.yml` only if needed
|
|
- Modify: `docs/deployment/architecture.mdx`
|
|
|
|
- [ ] **Step 1: Make Docker default explicit**
|
|
|
|
In `Dockerfile`, add:
|
|
|
|
```dockerfile
|
|
ENV NODE_ENV="production" \
|
|
SERVER_MODE="monolith" \
|
|
PORT=3000 \
|
|
LOCAL_STORAGE_PATH=/app/data
|
|
```
|
|
|
|
- [ ] **Step 2: Document backend-only override**
|
|
|
|
In `docs/deployment/architecture.mdx`, add:
|
|
|
|
```mdx
|
|
## Server Mode
|
|
|
|
`SERVER_MODE=monolith` is the default Docker mode.
|
|
|
|
`SERVER_MODE=backend-only` disables static serving and SPA fallback. Use this behind an external static host or edge frontend.
|
|
```
|
|
|
|
- [ ] **Step 3: Verify Docker build path at least compiles**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
pnpm build
|
|
```
|
|
|
|
Expected: build passes. If full repo build is too noisy due unrelated dirty work, run:
|
|
|
|
```bash
|
|
pnpm --filter web build
|
|
pnpm --filter server build
|
|
```
|
|
|
|
Expected: both pass.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add Dockerfile docs/deployment/architecture.mdx
|
|
git commit -m "chore(docker): keep monolith server mode as default"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 7: Final Verification And Stale Reference Search
|
|
|
|
**Files:** no new files.
|
|
|
|
- [ ] **Step 1: Run focused tests**
|
|
|
|
```bash
|
|
pnpm --filter @reactive-resume/server test
|
|
pnpm --filter server test -- src/handlers/auth.test.ts src/index.test.ts src/app.test.ts
|
|
pnpm --filter @reactive-resume/scripts test -- schema/generate.test.ts
|
|
```
|
|
|
|
Expected: all pass.
|
|
|
|
- [ ] **Step 2: Run focused typechecks**
|
|
|
|
```bash
|
|
pnpm --filter @reactive-resume/server typecheck
|
|
pnpm --filter server typecheck
|
|
pnpm --filter @reactive-resume/scripts typecheck
|
|
pnpm --filter @reactive-resume/auth typecheck
|
|
pnpm --filter web typecheck
|
|
```
|
|
|
|
Expected: all pass.
|
|
|
|
- [ ] **Step 3: Verify static schema build**
|
|
|
|
```bash
|
|
pnpm --filter @reactive-resume/scripts schema:generate
|
|
pnpm --filter web build
|
|
test -f apps/web/dist/schema.json
|
|
cmp packages/schema/schema.json apps/web/public/schema.json
|
|
```
|
|
|
|
Expected: all pass.
|
|
|
|
- [ ] **Step 4: Search stale route references**
|
|
|
|
```bash
|
|
rg '"/auth/oauth"|/auth/oauth|handleSchemaJson|app\\.get\\("/schema\\.json"|SERVER_MODE|backend-only|monolith' apps packages docs Dockerfile AGENTS.md
|
|
```
|
|
|
|
Expected:
|
|
- No runtime `/auth/oauth` references remain.
|
|
- No `handleSchemaJson` references remain.
|
|
- `SERVER_MODE`, `backend-only`, and `monolith` appear only in app composition, Docker, and docs.
|
|
|
|
- [ ] **Step 5: Run non-mutating formatting/lint check on touched files**
|
|
|
|
Use the repo's Biome config without writing:
|
|
|
|
```bash
|
|
pnpm exec biome check apps/server/src apps/web/vite.config.ts packages/auth/src/config.ts packages/scripts packages/server docs/deployment AGENTS.md Dockerfile
|
|
```
|
|
|
|
Expected: no errors. If formatting errors appear, run Biome write only on touched source/doc files, then rerun this non-mutating check.
|
|
|
|
---
|
|
|
|
## Self-Review
|
|
|
|
- Spec coverage:
|
|
- Preserve single Docker image: Tasks 1 and 6.
|
|
- Enable edge/S3/static frontend plus Node backend: Tasks 1, 2, 5, and 6.
|
|
- Keep backend Node-first: Tasks 5 and 6.
|
|
- Return 404 from backend for unknown split-mode routes: Task 5.
|
|
- Move `/auth/oauth` to `/api/auth/oauth`: Task 3.
|
|
- Make `/schema.json` static: Task 4.
|
|
- Keep `/.well-known/*` backend-owned: Tasks 1 and 5.
|
|
- Placeholder scan:
|
|
- No TBD/TODO/fill-in steps remain.
|
|
- Type consistency:
|
|
- Route ownership terms are consistently `backend`, `static`, and `spa`.
|
|
- Server modes are consistently `monolith` and `backend-only`.
|
|
- The OAuth bridge route is consistently `/api/auth/oauth`.
|
|
- Static schema targets are consistently `packages/schema/schema.json` and `apps/web/public/schema.json`.
|