diff --git a/docs/superpowers/specs/2026-05-02-b1-nextjs-scaffold-design.md b/docs/superpowers/specs/2026-05-02-b1-nextjs-scaffold-design.md
new file mode 100644
index 0000000..512e7ee
--- /dev/null
+++ b/docs/superpowers/specs/2026-05-02-b1-nextjs-scaffold-design.md
@@ -0,0 +1,359 @@
+# B1: Next.js Scaffold + Side-by-Side Deploy + Proxy Parity Design
+
+**Date:** 2026-05-02
+**Status:** Approved (design)
+**Sequel to:** [2026-05-02-prod-hardening-c1-c5-c6-design.md](2026-05-02-prod-hardening-c1-c5-c6-design.md)
+**Followed by:** B2 (UI port), B3 (PWA), B4 (cutover — delete `app/cm_web_view.py`, rename `cm-web-next` → `cm-web`).
+
+## Problem
+
+`app/cm_web_view.py` is a 758-line Flask app with inline HTML/CSS/JS. The user wants to migrate the UI to Next.js for a modern stack (App Router, React, Tailwind, PWA-ready). The full migration is multi-cycle; this cycle (B1) only ships the **scaffold and deploy story** — empty UI, parity-only proxies — so we can verify the build/deploy pipeline before pouring effort into the UI port (B2). The existing Flask UI keeps working in parallel during B2/B3.
+
+## Goal
+
+Stand up a new `cm-web-next` Docker service running a Next.js 15 app at `web/` in this repo, exposed on host port `${CM_WEB_NEXT_HOST_PORT:-8010}` (default 8010 — non-conflicting with 8000/dev, 8001/rex, 8005/siong). The app:
+
+1. Renders a single placeholder page so the operator can confirm the service is up.
+2. Provides four Route Handler endpoints that proxy to `api-server:3000` with **byte-equivalent behavior** to the existing Flask `cm_web_view.py` proxies — `GET /api/acc/`, `GET /api/user/`, `POST /api/update-acc-data`, `POST /api/update-user-data`. (B2 will start using these from React; B4 will retire the Flask versions.)
+3. Builds reproducibly through `scripts/publish.sh` like the four existing services.
+4. Comes up automatically with `bash scripts/dev.sh up`.
+
+The existing `cm-web` (Flask) service is untouched. Both services run side-by-side in dev and prod until B4 cuts over.
+
+## Non-Goals
+
+- Any actual UI work. The page reads `
cm-web-next scaffold
` plus a one-line confirmation that the API proxies are reachable. Tabs, tables, inline editing, sort, refresh — all in B2.
+- PWA / `manifest.webmanifest` / service worker / icons — all in B3.
+- Auth — handled by aaPanel (C3 basic auth covers the new vhost too).
+- Tests in B1. The scaffold is mostly config and glue; integration smoke is sufficient. B2 will introduce vitest when there's actual UI logic to test.
+- Migrating `app/cm_web_view.py` or removing the existing `cm-web` Flask service. B4's job after the UI is fully ported.
+- Renaming the `cm-web-next` service to `cm-web` during this cycle. The rename is part of B4 along with the Flask retirement.
+
+## Architecture
+
+### Repo layout
+
+```
+cm_bot_v2/
+├── app/ ← Python services (unchanged)
+├── docker/
+│ ├── api/, web/, telegram/, transfer/ ← Python service Dockerfiles
+│ └── web-next/Dockerfile ← NEW
+├── web/ ← NEW: full Next.js 15 project root
+│ ├── package.json
+│ ├── package-lock.json
+│ ├── next.config.ts
+│ ├── tsconfig.json
+│ ├── tailwind.config.ts
+│ ├── postcss.config.mjs
+│ ├── .gitignore
+│ ├── .dockerignore
+│ ├── app/
+│ │ ├── layout.tsx
+│ │ ├── page.tsx
+│ │ ├── globals.css
+│ │ └── api/
+│ │ └── [...path]/
+│ │ └── route.ts ← Catch-all proxy to api-server
+│ └── public/ ← (empty for B1; B3 adds icons)
+├── docker-compose.yml ← + cm-web-next service
+├── docker-compose.override.yml ← + cm-web-next build directive
+├── envs//.env.example ← + CM_WEB_NEXT_HOST_PORT
+└── scripts/
+ ├── dev.sh ← MODIFIED: include cm-web-next in up/logs
+ └── publish.sh ← MODIFIED: + cm-web-next image
+```
+
+`web/` is at the repo root (per L3 location decision), parallel to `app/`. It is its own JavaScript/TypeScript project with its own `package.json`, completely independent of the Python tree.
+
+### Tech stack
+
+| Choice | Value | Rationale |
+|---|---|---|
+| Next.js | 15.x stable | Modern, App Router, Route Handlers |
+| Language | TypeScript | Next.js default; tooling assumes it |
+| Styling | Tailwind CSS v4 | What `create-next-app` ships in 2026 |
+| Routing | App Router (`app/` dir) | Modern; B2/B3 conventions cleaner |
+| Build output | `output: 'standalone'` in `next.config.ts` | Slim runtime image (~150MB vs 1GB+) |
+| Package manager | npm | Zero ceremony, default tooling |
+| Node | 22 LTS (in container) | Current LTS line |
+| Trailing slashes | `trailingSlash: true` | Parity with Flask `@app.route('/api/acc/')` paths |
+
+### Route Handler proxy
+
+A single catch-all Route Handler at `web/app/api/[...path]/route.ts` forwards all `/api/*` requests to `api-server:3000` (the upstream service name on the compose network). Implementation sketch:
+
+```typescript
+import { NextRequest, NextResponse } from "next/server";
+
+const API_BASE_URL = process.env.API_BASE_URL ?? "http://api-server:3000";
+
+async function forward(request: NextRequest, path: string[]) {
+ const targetPath = "/" + path.join("/");
+ const trailingSlash = request.nextUrl.pathname.endsWith("/") ? "/" : "";
+ const target = `${API_BASE_URL}${targetPath}${trailingSlash}`;
+
+ const init: RequestInit = {
+ method: request.method,
+ headers: { "content-type": request.headers.get("content-type") ?? "application/json" },
+ };
+ if (request.method !== "GET" && request.method !== "HEAD") {
+ init.body = await request.text();
+ }
+
+ try {
+ const upstream = await fetch(target, init);
+ const body = await upstream.text();
+ return new NextResponse(body, {
+ status: upstream.status,
+ headers: { "content-type": upstream.headers.get("content-type") ?? "application/json" },
+ });
+ } catch (err) {
+ return NextResponse.json({ error: String(err) }, { status: 500 });
+ }
+}
+
+export async function GET(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
+ return forward(req, (await ctx.params).path);
+}
+export async function POST(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
+ return forward(req, (await ctx.params).path);
+}
+```
+
+Why catch-all instead of one file per endpoint:
+
+- The four endpoints today are mechanically identical proxies; writing four near-duplicate files is a YAGNI violation.
+- B2 may need additional methods or per-route logic; that's the time to split (and split only the routes that need it).
+- The error shape (`{"error": str}` with HTTP 500) matches the Flask version (`return jsonify({"error": str(e)}), 500`).
+
+The trailing-slash preservation means `GET /api/acc/` proxies to `api-server:3000/acc/` (Flask's exact pattern); `POST /api/update-acc-data` proxies to `api-server:3000/update-acc-data`. Same shape as `cm_web_view.py:proxy_acc()`, `proxy_update_acc()`, etc.
+
+### Empty UI page
+
+`web/app/page.tsx` renders a centered card confirming the service is up:
+
+```tsx
+export default function Home() {
+ return (
+
+
+
cm-web-next scaffold
+
+ B1 scaffold. UI lands in B2.
+
+
+ Try{" "}
+
+ /api/acc/
+ {" "}
+ to verify the proxy reaches{" "}
+ api-server:3000.
+
+
+
+ );
+}
+```
+
+`web/app/layout.tsx` is the standard `create-next-app` boilerplate with ``, body class for Tailwind defaults, and a tab title `CM Bot V2`.
+
+`web/app/globals.css` is the Tailwind v4 import (`@import "tailwindcss"`).
+
+### Dockerfile
+
+Multi-stage build at `docker/web-next/Dockerfile`, modeled after Next.js's official Dockerfile example. Uses `output: 'standalone'` so the runtime image only carries the standalone server bundle (~150MB total).
+
+```dockerfile
+# syntax=docker/dockerfile:1.7
+
+# --- deps ---
+FROM node:22-alpine AS deps
+WORKDIR /app
+COPY web/package.json web/package-lock.json* ./
+RUN npm ci
+
+# --- build ---
+FROM node:22-alpine AS build
+WORKDIR /app
+ENV NEXT_TELEMETRY_DISABLED=1
+COPY --from=deps /app/node_modules ./node_modules
+COPY web/ ./
+RUN npm run build
+
+# --- runtime ---
+FROM node:22-alpine AS runtime
+WORKDIR /app
+ENV NODE_ENV=production
+ENV NEXT_TELEMETRY_DISABLED=1
+ENV PORT=3000
+ENV HOSTNAME=0.0.0.0
+COPY --from=build /app/.next/standalone ./
+COPY --from=build /app/.next/static ./.next/static
+COPY --from=build /app/public ./public
+EXPOSE 3000
+CMD ["node", "server.js"]
+```
+
+The standalone server listens on `$PORT`. We expose 3000 inside the container and bind to a host port via compose (default 8010 → container 3000). Naming note: container port 3000 is internal; nothing else at this scale uses 3000 because api-server's host port was dropped in C5.
+
+### `next.config.ts`
+
+```ts
+import type { NextConfig } from "next";
+
+const config: NextConfig = {
+ output: "standalone",
+ trailingSlash: true,
+};
+
+export default config;
+```
+
+### Compose changes
+
+**`docker-compose.yml`** — add `cm-web-next` service near the existing `web-view`:
+
+```yaml
+ # Next.js Web View (side-by-side with web-view during B-cycle migration).
+ web-next:
+ image: "${CM_IMAGE_PREFIX:-your-registry/namespace}/cm-web-next:${DOCKER_IMAGE_TAG:-latest}"
+ container_name: ${CM_DEPLOY_NAME:-cm}-web-next
+ restart: unless-stopped
+ ports:
+ - "${CM_WEB_NEXT_HOST_PORT:-8010}:3000"
+ environment:
+ NODE_ENV: production
+ NEXT_TELEMETRY_DISABLED: "1"
+ API_BASE_URL: http://api-server:3000
+ networks:
+ - bot-network
+ depends_on:
+ - api-server
+```
+
+**`docker-compose.override.yml`** — add a `web-next` block with build directive (parallel to existing services):
+
+```yaml
+ web-next:
+ build:
+ context: .
+ dockerfile: docker/web-next/Dockerfile
+ image: "${CM_IMAGE_PREFIX:-local}/cm-web-next:${DOCKER_IMAGE_TAG:-dev}"
+ # No `command:` override — Next.js standalone server is the dev runtime
+ # for B1. (B2 may add a `command: ["npm", "run", "dev"]` to enable
+ # hot-reload; not in this cycle.)
+```
+
+The compose service name is `web-next` (not `cm-web-next`) for parity with the existing pattern (`web-view`, `api-server`, `telegram-bot`, `transfer-bot`). Container name resolves to `${CM_DEPLOY_NAME}-web-next` via the existing pattern.
+
+### Env file changes
+
+Add `CM_WEB_NEXT_HOST_PORT` to all three `.env.example` templates, with deployment-appropriate defaults:
+
+| File | New line |
+|---|---|
+| `envs/dev/.env.example` | `CM_WEB_NEXT_HOST_PORT=8010` |
+| `envs/rex/.env.example` | `CM_WEB_NEXT_HOST_PORT=8011` |
+| `envs/siong/.env.example` | `CM_WEB_NEXT_HOST_PORT=8012` |
+
+The committed templates document the convention; each operator's gitignored `.env` is updated by hand (or by `cp envs//.env.example .env` for fresh clones).
+
+### `scripts/dev.sh`
+
+Modify the `up`, `logs`, and `up` invocation in `reset-db` to include `web-next`:
+
+```bash
+ up)
+ "${COMPOSE[@]}" up -d --build mysql api-server web-view web-next
+ "${COMPOSE[@]}" ps
+ ;;
+ reset-db)
+ "${COMPOSE[@]}" down --volumes --remove-orphans
+ "${COMPOSE[@]}" up -d --build mysql api-server web-view web-next
+ ;;
+ logs)
+ "${COMPOSE[@]}" logs -f mysql api-server web-view web-next
+ ;;
+```
+
+### `scripts/publish.sh`
+
+Append `web-next` to the `SERVICES` array:
+
+```bash
+SERVICES=(
+ "api docker/api/Dockerfile"
+ "telegram docker/telegram/Dockerfile"
+ "web docker/web/Dockerfile"
+ "transfer docker/transfer/Dockerfile"
+ "web-next docker/web-next/Dockerfile"
+)
+```
+
+The image name template `${REGISTRY_PREFIX}/cm-${SERVICE}:${IMAGE_TAG}` produces `gitea.04080616.xyz/yiekheng/cm-web-next:`, matching the compose `image:` reference.
+
+### `AGENTS.md` updates
+
+- Add a bullet under "Project Structure & Module Organization" describing `web/` and the side-by-side `cm-web-next` service.
+- Add a one-line note under "Dev Tier" pointing to `http://localhost:8010/` for the new UI alongside `http://localhost:8000/` for the legacy Flask UI.
+
+### `web/.gitignore` and `web/.dockerignore`
+
+Standard Next.js ignores. `web/.gitignore` includes `.next/`, `node_modules/`, build/test outputs. `web/.dockerignore` excludes `node_modules/`, `.next/`, `.git/`, and the rest of the repo (we only need `web/` in the build context, but Docker copies what we ask for via the explicit `COPY web/...` line).
+
+## Files Created / Modified
+
+| File | Operation |
+|---|---|
+| `web/package.json` | Create — Next.js 15 deps, scripts |
+| `web/package-lock.json` | Create — generated by `npm install` |
+| `web/next.config.ts` | Create — `standalone` + `trailingSlash` |
+| `web/tsconfig.json` | Create — `create-next-app` defaults |
+| `web/tailwind.config.ts` | Create — Tailwind v4 default |
+| `web/postcss.config.mjs` | Create — Tailwind v4 PostCSS plugin |
+| `web/app/layout.tsx` | Create — title + Tailwind globals |
+| `web/app/page.tsx` | Create — placeholder card |
+| `web/app/globals.css` | Create — `@import "tailwindcss";` |
+| `web/app/api/[...path]/route.ts` | Create — catch-all proxy |
+| `web/.gitignore` | Create — Next.js standard |
+| `web/.dockerignore` | Create — exclude node_modules / .next |
+| `docker/web-next/Dockerfile` | Create — multi-stage Node 22 alpine |
+| `docker-compose.yml` | Modify — add `web-next` service |
+| `docker-compose.override.yml` | Modify — add `web-next` build directive |
+| `envs/dev/.env.example` | Modify — `CM_WEB_NEXT_HOST_PORT=8010` |
+| `envs/rex/.env.example` | Modify — `CM_WEB_NEXT_HOST_PORT=8011` |
+| `envs/siong/.env.example` | Modify — `CM_WEB_NEXT_HOST_PORT=8012` |
+| `scripts/dev.sh` | Modify — include `web-next` in up/logs/reset-db |
+| `scripts/publish.sh` | Modify — append `web-next` to `SERVICES` |
+| `AGENTS.md` | Modify — note new service + UI URL |
+
+No file removals. Nothing in `app/` is touched.
+
+## Verification
+
+1. **`web/` builds locally without docker.** `cd web && npm install && npm run build` succeeds (smoke for someone editing TS without rebuilding the container each time).
+2. **`bash scripts/dev.sh up` brings up five services.** `mysql`, `api-server`, `web-view` (Flask, port 8000), `web-next` (Next.js, port 8010). `docker compose ps` shows all five `running`.
+3. **Empty page renders.** `curl -s http://localhost:8010/ | grep -E "cm-web-next scaffold|B1 scaffold"` returns hits. Open in a browser → centered card visible with the link.
+4. **Proxy parity.** `curl -s http://localhost:8010/api/acc/ | head -c 200` returns the same JSON shape as `curl -s http://localhost:8000/api/acc/ | head -c 200` (both proxy to `api-server:3000/acc/`).
+5. **POST proxy.** `curl -i -X POST -H 'Content-Type: application/json' -d '{"username":"13c1000","password":"x","status":"","link":""}' http://localhost:8010/api/update-acc-data` returns the same response (and same exit status) as the same POST against the Flask `web-view` on port 8000.
+6. **Image publishes.** `bash scripts/publish.sh dev` publishes `cm-web-next:dev` alongside the other four images. (Skip in CI; smoke check on the operator's machine when ready.)
+7. **Old `cm-web` still works.** `curl -s http://localhost:8000/` still returns the Flask HTML page. Side-by-side preserved.
+8. **Prod parity check.** `docker compose -f docker-compose.yml config | grep -E "web-next" | head` shows the new service. `docker compose -f docker-compose.yml config | grep "ports:" -A 1` confirms `web-next` is on `${CM_WEB_NEXT_HOST_PORT:-8010}:3000` and `web-view` is unchanged on `${CM_WEB_HOST_PORT:-8001}:8000`.
+
+## Risk
+
+Low.
+
+- **Side-by-side means double the surface for now.** Two web services running, two host ports, two images. The cost is real but bounded — once B2 is feature-complete and B4 cuts over, the old `cm-web` retires and we're back to one. The alternative (in-place rewrite) was higher risk because broken B2 commits would break prod.
+- **Catch-all Route Handler regex semantics.** Next.js's `[...path]` matches any depth; the explicit Flask routes are flat (e.g., `/api/acc/`). If any future api-server endpoint accidentally collides with a Next.js convention path (`/api/_next/...`, `/api/__nextjs_original-stack-frames`), behavior could be surprising. None of these conflicts exist today (api-server's routes are `/acc/`, `/user/`, `/update-acc-data`, `/update-user-data`).
+- **Trailing slashes.** `trailingSlash: true` in Next.js means `/api/acc` redirects to `/api/acc/` with a 308. The Flask version always required the slash. Behavior is parity-equivalent for clients that include the slash; clients that don't get a redirect they didn't get before. The UI we control will always include it.
+- **Build context size.** Docker build context includes the whole repo unless we set up `.dockerignore`. The repo's existing `.dockerignore` excludes `__pycache__`, `*.py[cod]`, `*.log`, `.git`, `logs/`, `node_modules/`. The `node_modules/` entry already prevents copying the host's `web/node_modules/` if the operator ran `npm install` on the host — good. We don't need a new repo-root `.dockerignore` change.
+
+## Out-of-Scope Follow-Ups
+
+- **B2** — port the UI: tabs, two tables, sortable columns, inline cell editing, refresh, stats cards, error states.
+- **B3** — PWA: `manifest.webmanifest`, `next-pwa` (or hand-rolled service worker), 192/512 icons, install prompt.
+- **B4** — cutover: delete `app/cm_web_view.py`, retire `cm-web` service, rename `cm-web-next` → `cm-web` (and same for the image), update aaPanel-hardening guide and `dev.sh` accordingly.
+- **Hot-reload in dev** — `command: ["npm", "run", "dev"]` in the override + bind-mount `web/` into the container. Useful but not necessary for B1; revisit when B2 starts and iteration speed matters.
+- **Vitest** — unit tests for components/route handlers. B2 lays the foundation when there's actual logic.