The AtmosphereConf talks your skyline missed
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: add Railway deployment implementation plan for #31

+681
+681
docs/superpowers/plans/2026-04-10-railway-deploy.md
··· 1 + # Deploy Understory to Railway Implementation Plan 2 + 3 + > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. 4 + 5 + **Goal:** Deploy Understory to Railway with staging + production environments, self-hosted OAuth client metadata, and a custom domain (`understory.watch`). 6 + 7 + **Architecture:** Code changes (standalone output, postbuild script, shared OAuth metadata builder, metadata route) land on a feature branch, get validated locally, then pushed to staging for Railway validation. Railway infrastructure (project, environments, env vars, domains) is configured via Railway MCP tools/CLI. Production goes live when staging is validated and the custom domain DNS propagates. 8 + 9 + **Tech Stack:** Next.js 16 (standalone output), Railway (Nixpacks), AT Protocol OAuth (`@atproto/oauth-client-node`) 10 + 11 + **Spec:** `docs/superpowers/specs/2026-04-10-railway-deploy.md` 12 + 13 + **Chainlink Issue:** #31 14 + 15 + --- 16 + 17 + ## File Structure 18 + 19 + | File | Action | Responsibility | 20 + |------|--------|----------------| 21 + | `next.config.ts` | Modify | Add `output: "standalone"`, `outputFileTracingIncludes` for `data/`, remove Tailscale dev origin | 22 + | `package.json` | Modify | Add `postbuild` script; change `start` to `node .next/standalone/server.js` | 23 + | `src/lib/auth/metadata.ts` | Create | Shared `buildClientMetadata(appUrl)` — single source of truth for OAuth metadata | 24 + | `src/app/oauth/client-metadata.json/route.ts` | Create | Self-hosted AT Protocol OAuth client metadata endpoint | 25 + | `src/lib/auth/client.ts` | Modify | Import `buildClientMetadata`, remove `OAUTH_CLIENT_ID` dependency | 26 + 27 + --- 28 + 29 + ## Chunk 1: Code changes (feature branch) 30 + 31 + ### Task 1: Create the feature branch 32 + 33 + - [ ] **Step 1: Create branch** 34 + 35 + ```bash 36 + git checkout main 37 + git pull --ff-only 38 + git checkout -b feat/railway-deploy 39 + ``` 40 + 41 + --- 42 + 43 + ### Task 2: Next.js standalone config + postbuild script 44 + 45 + **Files:** 46 + - Modify: `next.config.ts` 47 + - Modify: `package.json` 48 + 49 + - [ ] **Step 1: Read the current `next.config.ts`** 50 + 51 + Read `/Users/bryan.guffey/Code/Understory/next.config.ts` and confirm the current contents match: 52 + 53 + ```ts 54 + import type { NextConfig } from "next"; 55 + 56 + const nextConfig: NextConfig = { 57 + experimental: { 58 + viewTransition: true, 59 + }, 60 + allowedDevOrigins: [ 61 + "127.0.0.1", 62 + "bryans-mac-mini.wildebeest-puffin.ts.net", 63 + ], 64 + }; 65 + 66 + export default nextConfig; 67 + ``` 68 + 69 + - [ ] **Step 2: Update `next.config.ts`** 70 + 71 + Replace the config object with: 72 + 73 + ```ts 74 + import type { NextConfig } from "next"; 75 + 76 + const nextConfig: NextConfig = { 77 + output: "standalone", 78 + outputFileTracingIncludes: { 79 + "/*": ["./data/**/*"], 80 + }, 81 + experimental: { 82 + viewTransition: true, 83 + }, 84 + allowedDevOrigins: [ 85 + "127.0.0.1", 86 + ], 87 + }; 88 + 89 + export default nextConfig; 90 + ``` 91 + 92 + Changes: 93 + - Added `output: "standalone"` — Railway needs this for optimized builds 94 + - Added `outputFileTracingIncludes` — tells `@vercel/nft` to include the `data/` directory in the standalone output trace 95 + - Removed `bryans-mac-mini.wildebeest-puffin.ts.net` from `allowedDevOrigins` — Tailscale dev URL shouldn't ship to production 96 + 97 + - [ ] **Step 3: Update `package.json` scripts** 98 + 99 + Read `package.json` first, then modify the `"scripts"` section. Use `npm pkg set` to non-destructively add/modify scripts: 100 + 101 + ```bash 102 + npm pkg set scripts.postbuild="cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public && cp -r data .next/standalone/data" 103 + npm pkg set scripts.start="node .next/standalone/server.js" 104 + ``` 105 + 106 + The `postbuild` script runs automatically after `npm run build` (npm lifecycle hook). It copies three things into the standalone directory that Next.js deliberately excludes: 107 + - `.next/static/` — CSS and JS bundles (without these, pages load with no styles or interactivity) 108 + - `public/` — static assets (currently empty, but correct for future use) 109 + - `data/` — talk + transcript JSON files (loaded at runtime via `fs.readFileSync(path.resolve(process.cwd(), "data", ...))`) 110 + 111 + The `start` script changes from `next start` to `node .next/standalone/server.js` because standalone mode produces its own server that doesn't need the full Next.js CLI. 112 + 113 + - [ ] **Step 4: Verify the build works locally** 114 + 115 + Run: `npm run build` 116 + 117 + Expected: 118 + - Build succeeds 119 + - `.next/standalone/` directory exists 120 + - `.next/standalone/server.js` exists 121 + - `.next/standalone/.next/static/` exists (from postbuild) 122 + - `.next/standalone/public/` exists (from postbuild) 123 + - `.next/standalone/data/` exists with `talks.json` and `transcripts/` (from postbuild) 124 + 125 + Verify the standalone directory contents: 126 + 127 + ```bash 128 + ls .next/standalone/server.js 129 + ls .next/standalone/.next/static/ 130 + ls .next/standalone/data/talks.json 131 + ls .next/standalone/data/transcripts/ | head -5 132 + ``` 133 + 134 + - [ ] **Step 5: Verify existing tests still pass** 135 + 136 + Run: `npm test` 137 + Expected: 40/40 tests pass (scoring tests, unrelated to deploy changes but confirms nothing broke). 138 + 139 + - [ ] **Step 6: Verify tsc and eslint** 140 + 141 + Run in parallel: 142 + - `npx tsc --noEmit` — Expected: clean 143 + - `npx eslint src/` — Expected: clean 144 + 145 + - [ ] **Step 7: Commit** 146 + 147 + ```bash 148 + git add next.config.ts package.json 149 + git commit -m "feat: add standalone output + postbuild script for Railway deploy 150 + 151 + - output: 'standalone' for optimized Railway builds 152 + - outputFileTracingIncludes for data/ directory 153 + - postbuild copies .next/static, public, data into standalone dir 154 + - start script changed to node .next/standalone/server.js 155 + - removed Tailscale dev origin from allowedDevOrigins" 156 + ``` 157 + 158 + --- 159 + 160 + ### Task 3: Shared OAuth metadata builder 161 + 162 + **Files:** 163 + - Create: `src/lib/auth/metadata.ts` 164 + 165 + - [ ] **Step 1: Create the shared metadata module** 166 + 167 + Create `src/lib/auth/metadata.ts`: 168 + 169 + ```ts 170 + /** 171 + * Shared AT Protocol OAuth client metadata builder. 172 + * 173 + * Used by both the client-metadata.json route handler (serves the metadata 174 + * to PDS servers during the OAuth flow) and the NodeOAuthClient constructor 175 + * (uses the metadata locally for the authorization handshake). Defined once 176 + * here so the two can never drift — drift causes cryptic OAuth mismatch errors. 177 + */ 178 + 179 + export const CLIENT_METADATA_PATH = "/oauth/client-metadata.json"; 180 + 181 + export function buildClientMetadata(appUrl: string) { 182 + const clientId = `${appUrl}${CLIENT_METADATA_PATH}`; 183 + return { 184 + client_id: clientId, 185 + client_name: "Understory", 186 + client_uri: appUrl, 187 + redirect_uris: [`${appUrl}/oauth/callback`], 188 + grant_types: ["authorization_code", "refresh_token"], 189 + response_types: ["code"], 190 + scope: "atproto transition:generic", 191 + application_type: "web" as const, 192 + dpop_bound_access_tokens: true, 193 + token_endpoint_auth_method: "none" as const, 194 + }; 195 + } 196 + ``` 197 + 198 + The `as const` assertions on `application_type` and `token_endpoint_auth_method` are needed because the `@atproto/oauth-client-node` SDK expects literal string types for these fields, not `string`. The other array fields (`redirect_uris`, `grant_types`, `response_types`) must remain mutable (no top-level `as const`) because the SDK's Zod input types expect mutable tuples. 199 + 200 + - [ ] **Step 2: Verify tsc is clean** 201 + 202 + Run: `npx tsc --noEmit` 203 + Expected: clean. 204 + 205 + --- 206 + 207 + ### Task 4: Self-hosted client metadata route 208 + 209 + **Files:** 210 + - Create: `src/app/oauth/client-metadata.json/route.ts` 211 + 212 + - [ ] **Step 1: Create the route directory** 213 + 214 + ```bash 215 + mkdir -p src/app/oauth/client-metadata.json 216 + ``` 217 + 218 + > **Note:** The folder name contains a literal dot (`.json`). This is intentional — Next.js App Router treats directory names as route segments, so this creates a route at `/oauth/client-metadata.json`. The dot is legal in directory names on all platforms. If any build tooling issue arises, the fallback is to rename to `client-metadata/` and update `CLIENT_METADATA_PATH` in `metadata.ts`. 219 + 220 + - [ ] **Step 2: Create the route handler** 221 + 222 + Create `src/app/oauth/client-metadata.json/route.ts`: 223 + 224 + ```ts 225 + import { NextResponse } from "next/server"; 226 + import { buildClientMetadata } from "@/lib/auth/metadata"; 227 + 228 + export async function GET() { 229 + const appUrl = process.env.APP_URL; 230 + if (!appUrl) { 231 + return NextResponse.json( 232 + { error: "APP_URL not configured" }, 233 + { status: 500 }, 234 + ); 235 + } 236 + return NextResponse.json(buildClientMetadata(appUrl)); 237 + } 238 + ``` 239 + 240 + - [ ] **Step 3: Verify the route works locally** 241 + 242 + Start the dev server: `npm run dev` (in another terminal or background). 243 + 244 + Then: 245 + ```bash 246 + curl http://127.0.0.1:3000/oauth/client-metadata.json 247 + ``` 248 + 249 + Expected: JSON response with `client_id` = `http://127.0.0.1:3000/oauth/client-metadata.json`, `redirect_uris` = `["http://127.0.0.1:3000/oauth/callback"]`, etc. 250 + 251 + If the route returns 404, the dot-in-folder-name isn't working. Use the fallback: rename the directory to `client-metadata/` (no `.json`), update `CLIENT_METADATA_PATH` in `metadata.ts` to `"/oauth/client-metadata"`, and re-test. 252 + 253 + Stop the dev server after verification. 254 + 255 + - [ ] **Step 4: Verify tsc and eslint** 256 + 257 + Run in parallel: 258 + - `npx tsc --noEmit` — Expected: clean 259 + - `npx eslint src/` — Expected: clean 260 + 261 + --- 262 + 263 + ### Task 5: Update OAuth client to use shared metadata 264 + 265 + **Files:** 266 + - Modify: `src/lib/auth/client.ts` 267 + 268 + - [ ] **Step 1: Read the current file** 269 + 270 + Read `src/lib/auth/client.ts`. Current contents (for reference): 271 + 272 + ```ts 273 + import { NodeOAuthClient } from "@atproto/oauth-client-node"; 274 + import type { NodeSavedState, NodeSavedSession } from "@atproto/oauth-client-node"; 275 + 276 + const stateStore = new Map<string, NodeSavedState>(); 277 + const sessionStore = new Map<string, NodeSavedSession>(); 278 + 279 + function createClient(): NodeOAuthClient { 280 + const clientId = process.env.OAUTH_CLIENT_ID; 281 + const appUrl = process.env.APP_URL; 282 + 283 + if (!clientId || !appUrl) { 284 + throw new Error( 285 + "Missing OAUTH_CLIENT_ID or APP_URL environment variables. " + 286 + "See docs/superpowers/specs/2026-04-06-oauth.md for setup instructions.", 287 + ); 288 + } 289 + 290 + return new NodeOAuthClient({ 291 + clientMetadata: { 292 + client_id: clientId, 293 + client_name: "Understory (Development)", 294 + client_uri: appUrl, 295 + redirect_uris: [`${appUrl}/oauth/callback`], 296 + grant_types: ["authorization_code", "refresh_token"], 297 + response_types: ["code"], 298 + scope: "atproto transition:generic", 299 + application_type: "web", 300 + dpop_bound_access_tokens: true, 301 + token_endpoint_auth_method: "none", 302 + }, 303 + stateStore: { ... }, 304 + sessionStore: { ... }, 305 + }); 306 + } 307 + ``` 308 + 309 + - [ ] **Step 2: Rewrite `createClient` to use `buildClientMetadata`** 310 + 311 + Replace the entire file with: 312 + 313 + ```ts 314 + import { NodeOAuthClient } from "@atproto/oauth-client-node"; 315 + import type { 316 + NodeSavedState, 317 + NodeSavedSession, 318 + } from "@atproto/oauth-client-node"; 319 + import { buildClientMetadata } from "./metadata"; 320 + 321 + const stateStore = new Map<string, NodeSavedState>(); 322 + const sessionStore = new Map<string, NodeSavedSession>(); 323 + 324 + function createClient(): NodeOAuthClient { 325 + const appUrl = process.env.APP_URL; 326 + 327 + if (!appUrl) { 328 + throw new Error( 329 + "Missing APP_URL environment variable. " + 330 + "Set to your app's public URL (e.g., https://understory.watch).", 331 + ); 332 + } 333 + 334 + return new NodeOAuthClient({ 335 + clientMetadata: buildClientMetadata(appUrl), 336 + stateStore: { 337 + async get(key: string) { 338 + return stateStore.get(key); 339 + }, 340 + async set(key: string, value: NodeSavedState) { 341 + stateStore.set(key, value); 342 + }, 343 + async del(key: string) { 344 + stateStore.delete(key); 345 + }, 346 + }, 347 + sessionStore: { 348 + async get(sub: string) { 349 + return sessionStore.get(sub); 350 + }, 351 + async set(sub: string, value: NodeSavedSession) { 352 + sessionStore.set(sub, value); 353 + }, 354 + async del(sub: string) { 355 + sessionStore.delete(sub); 356 + }, 357 + }, 358 + }); 359 + } 360 + 361 + // Cache on globalThis to survive Next.js hot reload 362 + declare global { 363 + var __oauthClient: NodeOAuthClient | undefined; 364 + } 365 + 366 + export function getOAuthClient(): NodeOAuthClient { 367 + if (!globalThis.__oauthClient) { 368 + globalThis.__oauthClient = createClient(); 369 + } 370 + return globalThis.__oauthClient; 371 + } 372 + ``` 373 + 374 + Key changes: 375 + - Removed `OAUTH_CLIENT_ID` env var — `client_id` is now derived from `APP_URL` via `buildClientMetadata` 376 + - Imported `buildClientMetadata` from `./metadata` (shared source of truth) 377 + - Updated error message to reference only `APP_URL` 378 + - `client_name` is now `"Understory"` (via the builder, not `"Understory (Development)"`) 379 + 380 + - [ ] **Step 3: Update local `.env`** 381 + 382 + Edit `.env` to remove `OAUTH_CLIENT_ID`: 383 + 384 + ``` 385 + APP_URL=http://127.0.0.1:3000 386 + ASSEMBLYAI_API_KEY=aa551c5e26674976a3103e6a4bfa7674 387 + ``` 388 + 389 + > **Note:** `.env` is gitignored — this change is local only. 390 + 391 + - [ ] **Step 4: Verify the full OAuth flow locally** 392 + 393 + Start the dev server: `npm run dev` 394 + 395 + 1. Open `http://127.0.0.1:3000` 396 + 2. Click "Log in" 397 + 3. Enter a Bluesky handle 398 + 4. Complete the OAuth flow (redirects to PDS → back to callback) 399 + 5. Verify: avatar appears in the Nav 400 + 6. Verify: `/api/crawl` returns data (authenticated) 401 + 402 + If the OAuth flow fails, check: 403 + - Does `curl http://127.0.0.1:3000/oauth/client-metadata.json` return valid metadata with matching `client_id`? 404 + - Is `APP_URL=http://127.0.0.1:3000` in `.env`? 405 + - Restart the dev server after `.env` changes (Next.js caches env vars) 406 + 407 + Stop the dev server after verification. 408 + 409 + - [ ] **Step 5: Verify tsc, eslint, and tests** 410 + 411 + Run in parallel: 412 + - `npx tsc --noEmit` — Expected: clean 413 + - `npx eslint src/` — Expected: clean 414 + - `npm test` — Expected: 40/40 pass 415 + 416 + - [ ] **Step 6: Run a full production build and verify** 417 + 418 + Run: `npm run build` 419 + 420 + Expected: build succeeds, standalone output created with all three copied directories. 421 + 422 + Verify the build output includes the metadata route: 423 + 424 + ``` 425 + Route (app) 426 + ... 427 + ├ ƒ /oauth/client-metadata.json ← NEW 428 + ... 429 + ``` 430 + 431 + - [ ] **Step 7: Commit all OAuth changes together** 432 + 433 + ```bash 434 + git add src/lib/auth/metadata.ts \ 435 + src/app/oauth/client-metadata.json/route.ts \ 436 + src/lib/auth/client.ts 437 + git commit -m "feat: self-hosted OAuth client metadata, drop OAUTH_CLIENT_ID 438 + 439 + - metadata.ts: shared buildClientMetadata(appUrl) function, single source 440 + of truth imported by both the metadata route and NodeOAuthClient. 441 + - /oauth/client-metadata.json route: serves metadata JSON to PDS servers 442 + during the OAuth authorization flow. 443 + - client.ts: derives client_id from APP_URL instead of reading a separate 444 + OAUTH_CLIENT_ID env var. client_name updated to 'Understory'. 445 + - Eliminates the cimd-service.fly.dev dependency for auth." 446 + ``` 447 + 448 + --- 449 + 450 + ## Chunk 2: Railway infrastructure 451 + 452 + ### Task 6: Create Railway project and environments 453 + 454 + This task uses Railway MCP tools or CLI. No git commits — infrastructure is configured out-of-band. 455 + 456 + - [ ] **Step 1: Check Railway CLI is authenticated** 457 + 458 + ```bash 459 + railway whoami 460 + ``` 461 + 462 + Expected: shows your Railway account. If not authenticated, run `railway login`. 463 + 464 + - [ ] **Step 2: Create the Railway project** 465 + 466 + Use the Railway MCP tool `create-project-and-link` or CLI: 467 + 468 + ```bash 469 + railway init 470 + ``` 471 + 472 + Name the project "understory". Link it to the GitHub repo `musicjunkieg/understory`. 473 + 474 + Alternatively, use the MCP tool: 475 + - Tool: `mcp__railway-mcp-server__create-project-and-link` 476 + - Project name: "understory" 477 + 478 + - [ ] **Step 3: Create staging environment** 479 + 480 + Use the Railway MCP tool `create-environment`: 481 + - Name: "staging" 482 + - Source branch: `staging` 483 + 484 + Or via CLI: 485 + ```bash 486 + railway environment create staging 487 + ``` 488 + 489 + - [ ] **Step 4: Create the `staging` branch and push it** 490 + 491 + ```bash 492 + git checkout main 493 + git checkout -b staging 494 + git push -u origin staging 495 + git checkout feat/railway-deploy 496 + ``` 497 + 498 + This creates the `staging` branch (initially identical to `main`) so Railway's staging environment has a branch to track. 499 + 500 + - [ ] **Step 5: Set environment variables — staging** 501 + 502 + Switch Railway context to staging, then set env vars: 503 + 504 + Use the MCP tool `set-variables` for the staging environment, or: 505 + 506 + ```bash 507 + railway link --environment staging 508 + railway variables set APP_URL=https://<staging-domain>.up.railway.app 509 + railway variables set HOSTNAME=0.0.0.0 510 + ``` 511 + 512 + > **Note:** You'll get the staging domain after the first deploy. Set `APP_URL` to a placeholder initially, deploy, read the generated domain from Railway's dashboard, then update `APP_URL` with the real domain and redeploy. 513 + 514 + - [ ] **Step 6: Set environment variables — production** 515 + 516 + Switch Railway context to production: 517 + 518 + ```bash 519 + railway link --environment production 520 + railway variables set APP_URL=https://understory.watch 521 + railway variables set HOSTNAME=0.0.0.0 522 + ``` 523 + 524 + - [ ] **Step 7: Generate a domain for the staging environment** 525 + 526 + Use the MCP tool `generate-domain` for the staging environment, or check the Railway dashboard for the auto-generated domain. 527 + 528 + Note the domain — it will be needed to update `APP_URL` in Step 5. 529 + 530 + --- 531 + 532 + ### Task 7: Push to staging and validate 533 + 534 + - [ ] **Step 1: Merge feature branch into staging** 535 + 536 + ```bash 537 + git checkout staging 538 + git merge feat/railway-deploy 539 + git push 540 + ``` 541 + 542 + Railway auto-deploys the staging environment. 543 + 544 + - [ ] **Step 2: Monitor the deploy** 545 + 546 + Use the MCP tool `list-deployments` or: 547 + 548 + ```bash 549 + railway logs --environment staging 550 + ``` 551 + 552 + Wait for the deploy to succeed. Check build logs for: 553 + - `next build` succeeds 554 + - `postbuild` script runs (copies static/public/data) 555 + - Server starts on the assigned port 556 + 557 + - [ ] **Step 3: Validate staging — pages** 558 + 559 + Using the staging `*.up.railway.app` URL: 560 + 561 + 1. Hit the landing page — should load with full styles 562 + 2. Hit `/talks` — should show 115+ talk grid 563 + 3. Hit `/talk/<any-rkey>` — should show transcript + HLS video player 564 + 565 + If pages load without styles → postbuild didn't copy `.next/static/`. Check build logs. 566 + If `/talks` crashes with ENOENT → postbuild didn't copy `data/`. Check build logs. 567 + 568 + - [ ] **Step 4: Validate staging — OAuth metadata** 569 + 570 + ```bash 571 + curl https://<staging-domain>.up.railway.app/oauth/client-metadata.json 572 + ``` 573 + 574 + Expected: JSON with `client_id` matching the staging URL. 575 + 576 + - [ ] **Step 5: Validate staging — OAuth flow** 577 + 578 + 1. Open the staging URL in a browser 579 + 2. Click "Log in" 580 + 3. Enter a Bluesky handle 581 + 4. Complete the OAuth flow 582 + 5. Verify: avatar appears in Nav 583 + 6. Hit `/api/crawl` in the browser console: `fetch('/api/crawl').then(r => r.json()).then(d => console.log(d))` 584 + 585 + If OAuth fails with a redirect mismatch → `APP_URL` in Railway doesn't match the actual staging domain. Update it and redeploy. 586 + 587 + - [ ] **Step 6: Validate staging — cache** 588 + 589 + Hit `/api/crawl` again. Should return `cached: true`. 590 + 591 + --- 592 + 593 + ### Task 8: Configure production domain and promote 594 + 595 + - [ ] **Step 1: Add custom domain to Railway production** 596 + 597 + Use the Railway dashboard or MCP tools to add `understory.watch` as a custom domain for the production environment. Railway will provide a CNAME target. 598 + 599 + - [ ] **Step 2: Configure DNS at your registrar** 600 + 601 + At your registrar, create a CNAME record: 602 + 603 + ``` 604 + understory.watch → <cname-target-from-railway> 605 + ``` 606 + 607 + Railway auto-provisions an SSL certificate via Let's Encrypt once DNS resolves. 608 + 609 + - [ ] **Step 3: Promote staging to production** 610 + 611 + ```bash 612 + git checkout main 613 + git merge staging 614 + git push 615 + ``` 616 + 617 + Railway auto-deploys the production environment. 618 + 619 + - [ ] **Step 4: Wait for DNS propagation** 620 + 621 + Check propagation: 622 + 623 + ```bash 624 + dig understory.watch CNAME 625 + ``` 626 + 627 + Expected: shows the Railway CNAME target. DNS propagation typically takes 5–30 minutes. 628 + 629 + - [ ] **Step 5: Validate production** 630 + 631 + Once DNS resolves: 632 + 633 + 1. `https://understory.watch` loads with valid SSL certificate (check the padlock) 634 + 2. `/talks` shows the talk grid with full styles 635 + 3. `/talk/<any-rkey>` shows transcript + HLS video 636 + 4. `/oauth/client-metadata.json` shows `client_id` = `https://understory.watch/oauth/client-metadata.json` 637 + 5. Full OAuth flow completes — login, avatar in Nav, `/api/crawl` returns data 638 + 6. Second `/api/crawl` call returns `cached: true` 639 + 640 + - [ ] **Step 6: Clean up the feature branch** 641 + 642 + ```bash 643 + git branch -d feat/railway-deploy 644 + git push origin --delete feat/railway-deploy 645 + ``` 646 + 647 + --- 648 + 649 + ### Task 9: Final verification and wrap-up 650 + 651 + - [ ] **Step 1: Confirm all acceptance criteria** 652 + 653 + Walk through spec §11 acceptance criteria: 654 + 655 + **Code:** 656 + - [ ] `next.config.ts` has `output: "standalone"` and `outputFileTracingIncludes` 657 + - [ ] `package.json` has `postbuild` script and standalone `start` command 658 + - [ ] `src/lib/auth/metadata.ts` exports `buildClientMetadata` 659 + - [ ] `/oauth/client-metadata.json` route works 660 + - [ ] `client.ts` uses `buildClientMetadata(appUrl)` (no `OAUTH_CLIENT_ID`) 661 + - [ ] Build produces standalone output with data + static + public 662 + - [ ] tsc clean, eslint clean, tests pass 663 + 664 + **Infrastructure:** 665 + - [ ] Railway project "understory" exists 666 + - [ ] Staging environment deploys from `staging` branch 667 + - [ ] Production environment deploys from `main` branch 668 + - [ ] Env vars set in both environments 669 + 670 + **Staging:** 671 + - [ ] Staging auto-deploys and all features work 672 + 673 + **Production:** 674 + - [ ] `https://understory.watch` loads with valid SSL 675 + - [ ] OAuth + crawl work on production 676 + 677 + - [ ] **Step 2: Use the finishing-a-development-branch skill** 678 + 679 + Since the feature branch was already merged into `staging` → `main` and deleted in Task 8 Step 6, the finishing skill's main value here is confirming everything is wrapped up. The work is already live. 680 + 681 + Invoke `superpowers:finishing-a-development-branch` to formally close out.