feature/fix-clippy-on-stable-channel
claude/smbcloud-github-app-k9641p
development
feature/apps
feature/build-release
feature/ci-docs
feature/ci-ruby-gems
feature/cicd
feature/completion
feature/dist-nuget
feature/fix-ci-sdk-pypi-rubygem
feature/fix-clippy-on-stable-channel
feature/fix-deployment-script
feature/github-email
feature/gresiq-v1
feature/project-new-structure
feature/rsync-deploy
feature/ruby-runner
feature/sdk-npm
feature/sdk-workflows
feature/smbcloud-cli-release-npm
feature/smbcloud-deploy
feature/wasm
main
release/v0.7.4
| 1 | --- |
| 2 | name: smbcloud-deploy-nextjs |
| 3 | description: Use when deploying or debugging Next.js apps on smbCloud, especially the dedicated `nextjs-ssr` flow that builds locally, uploads `.next/standalone` via rsync, restarts PM2 over SSH, and serves traffic behind Nginx. |
| 4 | --- |
| 5 | |
| 6 | # smbCloud Deploy Next.js |
| 7 | |
| 8 | Use this skill when work touches Next.js deployment on smbCloud. |
| 9 | |
| 10 | Applies to: |
| 11 | |
| 12 | - `kind = "nextjs-ssr"` deploy targets |
| 13 | - `.smb/config.toml` deploy configuration |
| 14 | - `process_deploy_nextjs_ssr.rs` |
| 15 | - PM2 process management for standalone builds |
| 16 | - Nginx proxying for SSR apps |
| 17 | - production-only issues caused by CORS, ports, or runtime mismatch |
| 18 | |
| 19 | ## Deployment model |
| 20 | |
| 21 | Keep the deploy hierarchy straight: |
| 22 | |
| 23 | - `Project` = umbrella workspace in smbCloud (not deployable) |
| 24 | - `DeployRepo` = the git repository inside that workspace |
| 25 | - `FrontendApp` = the deployable app unit, identified by `frontend_app_id` in config |
| 26 | - `Deployment` = one release event, linked to a FrontendApp via `frontend_app_id` |
| 27 | |
| 28 | The canonical chain is: Deployment → FrontendApp → DeployRepo → Project |
| 29 | |
| 30 | For `nextjs-ssr`, smbCloud does not use git-push deploy. |
| 31 | |
| 32 | The CLI route in `process_deploy.rs` short-circuits on: |
| 33 | |
| 34 | - `kind = "nextjs-ssr"` |
| 35 | |
| 36 | That path builds locally, then: |
| 37 | |
| 38 | - rsyncs `.next/standalone/` into the remote app directory root |
| 39 | - rsyncs `.next/static/` into the runtime directory's `.next/static/` |
| 40 | - rsyncs `public/` into the runtime directory's `public/` |
| 41 | - runs `ssh` and restarts the app with PM2 |
| 42 | |
| 43 | When `outputFileTracingRoot` points above the app directory, Next may preserve the source path inside `.next/standalone/` (for example `.next/standalone/web/server.js`). The deploy path must keep that nested runtime layout intact. |
| 44 | |
| 45 | Do not describe this flow as git based unless the project is actually using the generic git deploy path. |
| 46 | |
| 47 | ## Required app configuration |
| 48 | |
| 49 | The Next.js app must produce a standalone server bundle. |
| 50 | |
| 51 | Create `next.config.mjs` or equivalent with: |
| 52 | |
| 53 | ```js |
| 54 | /** @type {import('next').NextConfig} */ |
| 55 | const nextConfig = { |
| 56 | output: "standalone", |
| 57 | }; |
| 58 | |
| 59 | export default nextConfig; |
| 60 | ``` |
| 61 | |
| 62 | Without `output: "standalone"`, the deploy code fails because `.next/standalone/` is missing. |
| 63 | |
| 64 | The app package scripts should support: |
| 65 | |
| 66 | - `build`: `next build` |
| 67 | |
| 68 | `pnpm` is the safe package manager for the current CLI implementation. |
| 69 | |
| 70 | ## Required smbCloud config |
| 71 | |
| 72 | Minimum `.smb/config.toml` for SSR: |
| 73 | |
| 74 | ```toml |
| 75 | [project] |
| 76 | id = 28 # workspace id (for API routing) |
| 77 | frontend_app_id = "uuid" # FrontendApp id (for deployment tracking) |
| 78 | deploy_repo_id = 123 # DeployRepo id (links to git repository record) |
| 79 | deployment_method = 1 |
| 80 | kind = "nextjs-ssr" |
| 81 | source = "." |
| 82 | package_manager = "pnpm" |
| 83 | pm2_app = "my-app" |
| 84 | path = "apps/web/my-app" |
| 85 | port = 3028 |
| 86 | ``` |
| 87 | |
| 88 | Important rules: |
| 89 | |
| 90 | - `kind = "nextjs-ssr"` is what activates the SSR deploy path |
| 91 | - `id` is the umbrella workspace ID, used for auth and API routing |
| 92 | - `frontend_app_id` identifies the FrontendApp record for this deploy target — include it so deployments are attributed to the correct app, not just the workspace |
| 93 | - `deploy_repo_id` identifies the DeployRepo record backing this app — include it when available |
| 94 | - `source` is the local Next.js app directory |
| 95 | - `path` is the remote destination directory on the server |
| 96 | - `pm2_app` is mandatory because the CLI restarts PM2 by name |
| 97 | - `port` should be set explicitly and must match nginx |
| 98 | - `deployment_method = 1` may still be present, but the `kind` routing is what matters most |
| 99 | |
| 100 | ## Expected remote layout |
| 101 | |
| 102 | After a successful deploy, the remote directory should look like one of these layouts: |
| 103 | |
| 104 | - flat runtime |
| 105 | - `/home/git/apps/web/<app>/server.js` |
| 106 | - `/home/git/apps/web/<app>/.next/static/...` |
| 107 | - `/home/git/apps/web/<app>/public/...` |
| 108 | - nested runtime when standalone preserves the source path |
| 109 | - `/home/git/apps/web/<app>/node_modules/...` |
| 110 | - `/home/git/apps/web/<app>/web/server.js` |
| 111 | - `/home/git/apps/web/<app>/web/.next/static/...` |
| 112 | - `/home/git/apps/web/<app>/web/public/...` |
| 113 | |
| 114 | The CLI uploads the contents of `.next/standalone/`, not the folder itself. In nested-runtime mode it also needs to keep PM2 compatibility for old root-level `server.js` and `.next/standalone/server.js` entrypoints. |
| 115 | |
| 116 | ## PM2 behavior |
| 117 | |
| 118 | The CLI's deploy path now deletes and starts fresh, preferring the server's |
| 119 | ecosystem config file when it exists. The CLI checks for `ecosystem.config.cjs` |
| 120 | first, then falls back to `ecosystem.config.js`. During deploy, the server-side |
| 121 | migration auto-renames `.js` to `.cjs` when applicable. |
| 122 | |
| 123 | Conceptually it behaves like: |
| 124 | |
| 125 | ```sh |
| 126 | cd "$APP_PATH" |
| 127 | mkdir -p logs |
| 128 | |
| 129 | if pm2 describe "$PM2_APP" > /dev/null 2>&1; then |
| 130 | pm2 delete "$PM2_APP" |
| 131 | fi |
| 132 | |
| 133 | if [ -f ecosystem.config.cjs ]; then |
| 134 | pm2 start ecosystem.config.cjs --only "$PM2_APP" --env production |
| 135 | elif [ -f ecosystem.config.js ]; then |
| 136 | pm2 start ecosystem.config.js --only "$PM2_APP" --env production |
| 137 | else |
| 138 | NODE_ENV=production PORT=<port> HOSTNAME=127.0.0.1 pm2 start node --name "$PM2_APP" -- server.js |
| 139 | fi |
| 140 | |
| 141 | pm2 save |
| 142 | ``` |
| 143 | |
| 144 | This has two consequences: |
| 145 | |
| 146 | - old git-based `post-receive` hooks are irrelevant for the SSR path |
| 147 | - the configured `port` in `.smb/config.toml` is used for the fallback start path |
| 148 | - the live server `ecosystem.config.cjs` (or `.js`) is the runtime source of truth when present |
| 149 | |
| 150 | ### Environment variables — ecosystem file (standard pattern) |
| 151 | |
| 152 | The server runs in a **multi-tenant setup**: multiple Next.js apps share one server, each managed as a separate PM2 process. The standard way to manage per-app environment variables is an `ecosystem.config.cjs` file kept **on the server only** (never committed to the app repo). |
| 153 | |
| 154 | Use the `.cjs` extension. This is required when the project's `package.json` has `"type": "module"`, because Node treats `.js` files as ESM in that case and `module.exports` syntax will fail. |
| 155 | |
| 156 | The flow works like this: |
| 157 | |
| 158 | 1. On first setup, SSH in, create the ecosystem file, start with it, and save: |
| 159 | |
| 160 | ```sh |
| 161 | ssh git@<server> |
| 162 | cd /home/git/apps/web/<app> |
| 163 | |
| 164 | cat > ecosystem.config.cjs << 'EOF' |
| 165 | module.exports = { |
| 166 | apps: [{ |
| 167 | name: "<pm2_app>", |
| 168 | script: "server.js", |
| 169 | cwd: "/home/git/apps/web/<app>", |
| 170 | env_production: { |
| 171 | NODE_ENV: "production", |
| 172 | PORT: 3026, |
| 173 | HOSTNAME: "127.0.0.1", |
| 174 | MY_SECRET: "...", |
| 175 | } |
| 176 | }] |
| 177 | } |
| 178 | EOF |
| 179 | |
| 180 | pm2 start ecosystem.config.cjs --env production |
| 181 | pm2 save |
| 182 | ``` |
| 183 | |
| 184 | 2. Every subsequent CLI deploy deletes the PM2 process and starts it fresh. The CLI checks for `ecosystem.config.cjs` first, then `ecosystem.config.js`. If neither exists, it falls back to `node server.js` with inline `NODE_ENV`, `PORT`, and `HOSTNAME`. |
| 185 | |
| 186 | 3. To change or add env vars, SSH in, edit `ecosystem.config.cjs`, then: |
| 187 | |
| 188 | ```sh |
| 189 | pm2 restart <pm2_app> --update-env |
| 190 | pm2 save |
| 191 | ``` |
| 192 | |
| 193 | The ecosystem file is the server-side source of truth for each app's runtime config. The CLI preserves both `ecosystem.config.cjs` and `ecosystem.config.js` on the server during rsync and must not delete either. |
| 194 | |
| 195 | ### Manual PM2 operations (without ecosystem file) |
| 196 | |
| 197 | If you are operating without an ecosystem file, prefer the same fresh-start model the CLI uses: |
| 198 | |
| 199 | ```sh |
| 200 | cd /home/git/apps/web/<app> |
| 201 | if pm2 describe <pm2_app> > /dev/null 2>&1; then |
| 202 | pm2 delete <pm2_app> |
| 203 | fi |
| 204 | PORT=<port> HOSTNAME=127.0.0.1 pm2 start node --name <pm2_app> -- server.js |
| 205 | pm2 save |
| 206 | ``` |
| 207 | |
| 208 | If you are using an ecosystem file and only changed env vars manually, this is still valid: |
| 209 | |
| 210 | ```sh |
| 211 | cd /home/git/apps/web/<app> |
| 212 | pm2 restart <pm2_app> --update-env |
| 213 | pm2 save |
| 214 | ``` |
| 215 | |
| 216 | ## Port management |
| 217 | |
| 218 | Use one source of truth for the runtime port. |
| 219 | |
| 220 | If an older ecosystem config (`.cjs` or `.js`) exists and contains both: |
| 221 | |
| 222 | - `args: "start -p 3026"` |
| 223 | - `env_production.PORT = 3025` |
| 224 | |
| 225 | that is a broken configuration. Standardize on one port value. |
| 226 | |
| 227 | Current rules: |
| 228 | |
| 229 | - fallback fresh-start path uses `port` from `.smb/config.toml` |
| 230 | - ecosystem-managed apps should set the same port in `env_production.PORT` (in `ecosystem.config.cjs`) |
| 231 | - nginx upstream must match that port |
| 232 | |
| 233 | ## Nginx for SSR |
| 234 | |
| 235 | Do not use a static-site Nginx config like: |
| 236 | |
| 237 | ```nginx |
| 238 | root /home/git/apps/web/my-app; |
| 239 | location / { |
| 240 | try_files $uri $uri/ =404; |
| 241 | } |
| 242 | ``` |
| 243 | |
| 244 | That is for static files, not Next.js SSR. |
| 245 | |
| 246 | Use reverse proxying to the local Node process instead: |
| 247 | |
| 248 | ```nginx |
| 249 | location / { |
| 250 | proxy_pass http://127.0.0.1:3026; |
| 251 | proxy_http_version 1.1; |
| 252 | |
| 253 | proxy_set_header Host $host; |
| 254 | proxy_set_header X-Real-IP $remote_addr; |
| 255 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 256 | proxy_set_header X-Forwarded-Proto $scheme; |
| 257 | |
| 258 | proxy_set_header Upgrade $http_upgrade; |
| 259 | proxy_set_header Connection "upgrade"; |
| 260 | |
| 261 | proxy_cache_bypass $http_upgrade; |
| 262 | } |
| 263 | ``` |
| 264 | |
| 265 | Nginx must proxy to the actual PM2 port. |
| 266 | |
| 267 | ### Do not `alias` `/_next/static` for nested standalone builds |
| 268 | |
| 269 | The proxy-everything block above is the safe default: let the standalone Node |
| 270 | server serve `/_next/static` itself. The common breakage is an nginx block that |
| 271 | tries to serve those assets from disk: |
| 272 | |
| 273 | ```nginx |
| 274 | # WRONG for a nested (monorepo) standalone build |
| 275 | location /_next/static { |
| 276 | alias /home/git/apps/web/<app>/.next/static; # 404s every JS/CSS |
| 277 | } |
| 278 | ``` |
| 279 | |
| 280 | When the app builds with `outputFileTracingRoot` (monorepo), standalone is |
| 281 | **nested**, so static actually lands at `…/<app>/<source>/.next/static` |
| 282 | (e.g. `…/<app>/apps/web/.next/static`), not `…/<app>/.next/static`. The alias |
| 283 | points at the wrong directory and every chunk 404s while the HTML still loads — |
| 284 | a confusing "page renders, no styles/JS" symptom. |
| 285 | |
| 286 | Fixes, in order of preference: |
| 287 | |
| 288 | 1. Drop the `/_next/static` `alias` block and proxy everything to the Node |
| 289 | process (the block above). Layout-independent — works for flat and nested. |
| 290 | 2. If you must serve static from disk, point the alias at the nested path: |
| 291 | `alias …/<app>/<source>/.next/static;`. |
| 292 | |
| 293 | Flat (single-repo, `source = "."`) builds keep static at `…/<app>/.next/static`, |
| 294 | so a disk alias there happens to be correct — but proxy-everything still works |
| 295 | and is the recommended uniform pattern. |
| 296 | |
| 297 | ### One file + SAN cert for multiple hostnames |
| 298 | |
| 299 | To serve several hostnames for the same app (e.g. a primary domain plus a |
| 300 | `*.5mb.app` alias) from one `server` block, they must share one certificate — a |
| 301 | `server` block can only present one cert. Issue a SAN cert listing both names, |
| 302 | then write one block: |
| 303 | |
| 304 | ```sh |
| 305 | sudo certbot certonly --nginx -d app.example.com -d app.example.net --cert-name myapp |
| 306 | ``` |
| 307 | |
| 308 | ```nginx |
| 309 | server { |
| 310 | listen 443 ssl http2; |
| 311 | server_name app.example.com app.example.net; |
| 312 | ssl_certificate /etc/letsencrypt/live/myapp/fullchain.pem; |
| 313 | ssl_certificate_key /etc/letsencrypt/live/myapp/privkey.pem; |
| 314 | # ... single proxy_pass block; Host $host passes the real hostname through |
| 315 | } |
| 316 | ``` |
| 317 | |
| 318 | Mixing two different certs (a per-domain cert plus a shared wildcard) forces two |
| 319 | `server` blocks. Run `certbot certonly` first, then write the config that |
| 320 | references the new lineage — pointing `ssl_certificate` at a path that does not |
| 321 | exist yet makes `nginx -t` fail. |
| 322 | |
| 323 | ## Common failure modes |
| 324 | |
| 325 | ### `.next/standalone` missing |
| 326 | |
| 327 | Cause: |
| 328 | |
| 329 | - no `output: "standalone"` in Next config |
| 330 | |
| 331 | Fix: |
| 332 | |
| 333 | - add standalone output |
| 334 | - rebuild locally |
| 335 | |
| 336 | ### `server.js` missing on the server |
| 337 | |
| 338 | Check: |
| 339 | |
| 340 | - whether the SSR deploy path actually ran |
| 341 | - whether `path` points to the directory you inspected |
| 342 | - whether `rsync` uploaded to a different location |
| 343 | |
| 344 | ### Missing `public/` |
| 345 | |
| 346 | `public/` is optional. The current deploy code skips it when the directory does not exist. |
| 347 | |
| 348 | If assets are missing in production, check whether the app actually has a `public/` directory locally and whether the expected files were copied into the remote `public/` directory. |
| 349 | |
| 350 | ### `Cannot find module 'next'` or `502 Bad Gateway` |
| 351 | |
| 352 | Cause: |
| 353 | |
| 354 | - `.next/standalone/node_modules/*` contains `pnpm` symlinks |
| 355 | - rsync preserved the symlinks instead of copying their targets |
| 356 | - or the standalone output itself is incomplete and missing traced runtime files |
| 357 | - PM2 starts `server.js`, Node cannot resolve `next`, and nginx returns `502 Bad Gateway` |
| 358 | |
| 359 | Fix: |
| 360 | |
| 361 | - use the dedicated `nextjs-ssr` CLI path in current `smbcloud-cli` |
| 362 | - make sure the standalone upload uses `rsync --copy-links` |
| 363 | - verify `node .next/standalone/server.js` works locally before deploy |
| 364 | - if local standalone fails too, add `outputFileTracingIncludes` for the missing runtime files and rebuild |
| 365 | - then restart PM2 and recheck `pm2 logs <pm2_app>` |
| 366 | |
| 367 | ### `Cannot find module 'styled-jsx'`, `@swc/helpers`, or other pnpm peer deps |
| 368 | |
| 369 | A common concrete failure looks like: |
| 370 | |
| 371 | - `code: 'MODULE_NOT_FOUND'` |
| 372 | - `path: '/home/git/apps/web/<app>/node_modules/@swc/helpers'` |
| 373 | |
| 374 | That usually means the standalone upload succeeded, but the top-level hoisted symlink for a scoped package was not recreated on the server. |
| 375 | |
| 376 | Cause: |
| 377 | |
| 378 | - pnpm stores peer dependencies inside `node_modules/.pnpm/<pkg>@<version>/node_modules/<pkg>` with internal symlinks that Node's standard `require.resolve()` cannot follow |
| 379 | - Next.js standalone traces the files into `.pnpm/` correctly, but does not create the flat `node_modules/<pkg>` entries needed for resolution |
| 380 | - this affects `styled-jsx`, `@swc/helpers`, `@next/env`, and other packages that `next` requires at startup |
| 381 | |
| 382 | Fix: |
| 383 | |
| 384 | - the CLI deploy script now auto-hoists in two passes: |
| 385 | - first mirror pnpm's virtual directory at `node_modules/.pnpm/node_modules/*` |
| 386 | - then fall back to scanning `node_modules/.pnpm/*/node_modules/*` for anything still missing |
| 387 | - this preserves pnpm's exact resolved package choice and fixes scoped packages like `@swc/helpers` as well as unscoped ones like `styled-jsx` |
| 388 | - no manual intervention needed for new deploys once the updated CLI is used |
| 389 | - to fix a server manually, prefer pnpm's virtual directory as the source of truth: |
| 390 | - create `node_modules/@scope/` directories as needed |
| 391 | - symlink `node_modules/<pkg>` or `node_modules/@scope/<pkg>` to the corresponding entry under `node_modules/.pnpm/node_modules/` |
| 392 | - only if that directory is missing, scan individual `.pnpm/<store-entry>/node_modules/*` folders as a fallback |
| 393 | |
| 394 | ### Wrong port after deploy |
| 395 | |
| 396 | Cause: |
| 397 | |
| 398 | - `port` in `.smb/config.toml`, server ecosystem config (`.cjs` or `.js`), and nginx upstream disagree |
| 399 | |
| 400 | Fix: |
| 401 | |
| 402 | - standardize the same port in all three places |
| 403 | - if using ecosystem config, make sure PM2 is actually starting from that file |
| 404 | |
| 405 | ### PM2 "module is not defined" or "require is not defined" |
| 406 | |
| 407 | Cause: |
| 408 | |
| 409 | - the project's `package.json` has `"type": "module"`, which makes Node treat `.js` files as ESM |
| 410 | - `ecosystem.config.js` uses `module.exports` (CommonJS), which is invalid under ESM |
| 411 | - deploy shims that use `require()` will also fail under ESM |
| 412 | |
| 413 | Fix: |
| 414 | |
| 415 | - rename `ecosystem.config.js` to `ecosystem.config.cjs` on the server |
| 416 | - deploy shims must use `import()` not `require()` |
| 417 | - the CLI now handles both automatically: it checks for `.cjs` first and the server-side migration auto-renames `.js` to `.cjs` during deploy |
| 418 | |
| 419 | ### `rsync` of `.next/standalone/` fails with status 23 (dangling pnpm symlink) |
| 420 | |
| 421 | A reproducible failure on pnpm monorepos. The deploy aborts at the standalone |
| 422 | upload with an empty stderr and: |
| 423 | |
| 424 | ``` |
| 425 | ✘ rsync of '.next/standalone/' failed (status 23): |
| 426 | ``` |
| 427 | |
| 428 | Run the same rsync by hand and you see the real cause: |
| 429 | |
| 430 | ``` |
| 431 | IO error encountered -- skipping file deletion |
| 432 | ``` |
| 433 | |
| 434 | Cause: |
| 435 | |
| 436 | - Next.js standalone tracing emits a virtual pnpm symlink such as |
| 437 | `.next/standalone/node_modules/.pnpm/node_modules/semver -> ../semver@6.3.1/node_modules/semver`, |
| 438 | but only the version it actually traced (e.g. `semver@7.7.3`) is copied into |
| 439 | the bundle. The `@6.3.1` target is never written, so the symlink dangles. |
| 440 | - The CLI rsyncs standalone with `--copy-links`, which tries to follow every |
| 441 | symlink. A dangling link is an IO error → rsync exits 23 and skips deletion. |
| 442 | - The package is not part of the traced runtime, so the link is safe to drop. |
| 443 | |
| 444 | Find them: |
| 445 | |
| 446 | ```sh |
| 447 | find .next/standalone/ -type l ! -exec test -e {} \; -print |
| 448 | ``` |
| 449 | |
| 450 | Fix: |
| 451 | |
| 452 | - The CLI now handles this automatically. `process_deploy_nextjs_ssr` runs a |
| 453 | step 3b (`prune_dangling_symlinks`) after the build and before the standalone |
| 454 | rsync: it walks `.next/standalone/` and deletes any symlink whose target does |
| 455 | not exist (real dirs are traversed; symlinked dirs are never followed). When |
| 456 | it removes anything it prints `Pruned N dangling symlink(s) …`. No manual step |
| 457 | is needed on current `smbcloud-cli`. |
| 458 | - Older `smb` (≤ 0.4.7) lacks the prune. On those, remove the link(s) before |
| 459 | upload and re-run the deploy — but the rebuild regenerates them, so it must |
| 460 | happen after `next build` and before rsync: |
| 461 | `find .next/standalone/ -type l ! -exec test -e {} \; -delete` |
| 462 | Driving the deploy by hand, delete then run the three rsyncs yourself. A |
| 463 | repo-local equivalent is a `postbuild` script that runs the same `find … |
| 464 | -delete`, since `smb` invokes `pnpm build` (which fires `postbuild`) in the |
| 465 | right window. |
| 466 | |
| 467 | ### Misleading deploy output |
| 468 | |
| 469 | The CLI may print generic deploy messages such as: |
| 470 | |
| 471 | - `Deploying > Building the app` |
| 472 | - `App is running` |
| 473 | |
| 474 | These messages do not mean git push happened. Confirm behavior from the code path, not the spinner text. |
| 475 | |
| 476 | ## Auth and production-only web issues |
| 477 | |
| 478 | If the Next.js app uses the wasm auth client from `smbcloud-auth-sdk-wasm`, production failures that show: |
| 479 | |
| 480 | - `Network error. Please check your internet connection and try again.` |
| 481 | |
| 482 | are often not actual connectivity failures. |
| 483 | |
| 484 | Check these first: |
| 485 | |
| 486 | - Rails CORS allows the production origin |
| 487 | - `Authorization` response header is exposed over CORS |
| 488 | - backend auth status codes match the Rust parser logic |
| 489 | |
| 490 | ### Rails requirement |
| 491 | |
| 492 | For browser clients using `POST /v1/client/users/sign_in`, if the token is returned in the `Authorization` response header, CORS must include: |
| 493 | |
| 494 | - production origins such as `https://ondeinference.com` |
| 495 | - `expose: %w[... Authorization]` |
| 496 | |
| 497 | Without that, browser fetch in wasm can surface as a fake network error. |
| 498 | |
| 499 | ### Rust requirement |
| 500 | |
| 501 | The shared Rust networking layer must not collapse all non-200 auth responses into `NetworkError`. |
| 502 | |
| 503 | If backend returns meaningful auth errors such as `401` invalid password, update the parser in `request_login` to preserve the real failure. |
| 504 | |
| 505 | ## Recommended validation |
| 506 | |
| 507 | Use the smallest checks that prove the contract. |
| 508 | |
| 509 | ### Next.js app |
| 510 | |
| 511 | - `pnpm build` |
| 512 | - verify standalone contains a runnable server entrypoint |
| 513 | - if `.next/standalone/server.js` exists, verify `node .next/standalone/server.js` starts locally |
| 514 | - if standalone preserves the source path, verify `node .next/standalone/<source>/server.js` starts locally |
| 515 | |
| 516 | ### smbcloud-cli |
| 517 | |
| 518 | - `cargo check -p smbcloud-network` |
| 519 | - `cargo check -p cli` when deploy code changed |
| 520 | |
| 521 | ### Server |
| 522 | |
| 523 | - `pm2 list` |
| 524 | - `pm2 logs <pm2_app>` |
| 525 | - `ls -la /home/git/apps/web/<app>` |
| 526 | - `ls -ld /home/git/apps/web/<app>/node_modules/next` |
| 527 | - `sudo nginx -t` |
| 528 | |
| 529 | ## Common mistakes |
| 530 | |
| 531 | - assuming `deployment_method = 1` alone activates SSR deploy |
| 532 | - forgetting `kind = "nextjs-ssr"` |
| 533 | - keeping an old git `post-receive` hook and expecting it to manage SSR deploys |
| 534 | - `alias`-ing `/_next/static` for a nested (monorepo) standalone build — static |
| 535 | is under `<app>/<source>/.next/static`, so the alias 404s; proxy everything instead |
| 536 | - assuming a dangling pnpm symlink in `.next/standalone/` still breaks the |
| 537 | deploy — current `smbcloud-cli` prunes them in step 3b before the |
| 538 | `rsync --copy-links` upload; only pre-prune manually on `smb` ≤ 0.4.7 |
| 539 | - giving two hostnames one `server` block without a shared SAN cert |
| 540 | - using static-file Nginx config for an SSR app |
| 541 | - rsyncing `.next/standalone/` without `--copy-links` and shipping broken `pnpm` symlinks to the server |
| 542 | - editing the repo copy of `ecosystem.config.cjs` and expecting a normal CLI deploy to overwrite the server copy — it is preserved intentionally |
| 543 | - editing the server `ecosystem.config.cjs` but the deploy script falling back to inline `node server.js` because the file path check is wrong |
| 544 | - keeping an `ecosystem.config.js` when the project uses ESM (`"type": "module"` in `package.json`) — rename it to `.cjs` or let the CLI migration handle it |
| 545 | - leaving port values inconsistent across PM2, Nginx, and `.smb/config.toml` |
| 546 | - forgetting to set `frontend_app_id` on the deploy target once app-level deployment tracking is available |
| 547 | - treating browser CORS failures as real internet outages |
| 548 | - deploying without `frontend_app_id` in config — the API will try to infer the app, but explicit is better than implicit |