Historical decision text is preserved. Accepted describes the decision, not deployment of every capability discussed. Consult the Architecture and Infrastructure tabs for the verified running system.
Date: 2026-05-10
Status: Accepted
Deciders: Daniel Meza
Context and Problem Statement
Urza's Wallet's primary acquisition channel is organic search. The platform must serve product comparison pages, category hubs, filter pages, and editorial content that are fully crawlable, fast to load (LCP (Largest Contentful Paint) < 2.5 s), and updated frequently as financial rates change. At the same time, the platform requires a logged-in dashboard with personalized saved comparisons and alerts — a use case better served by client-side rendering.
We need a frontend framework and rendering strategy that:
- Serves SEO (Search Engine Optimization)-optimized, static or server-rendered pages for public content.
- Provides rich, interactive client-side views for authenticated users.
- Handles the OIDC (OpenID Connect) session cookie flow securely without exposing tokens to the browser.
- Aggregates multiple API calls into page-level payloads server-side (BFF pattern) to reduce client round trips.
- Integrates cleanly with Railway's deployment model (single Docker container per service).
Decision Drivers
- SEO requirement is non-negotiable: Financial comparison pages must be indexed by Google to generate organic traffic. A pure SPA (Single Page Application, e.g., React + Vite, Angular) would require JavaScript execution before content is visible, harming Core Web Vitals and crawlability.
- Security requirement: Session tokens (Clerk JWT (JSON Web Token)) must never be sent to the browser. The BFF pattern, colocated with the Next.js app, is the correct place to validate Clerk sessions and keep tokens server-side.
- Team efficiency: A unified TypeScript codebase for both frontend and the BFF reduces context switching. The alternative (separate Express BFF + React SPA) adds a third deployable unit and two codebases to maintain.
- Railway compatibility: Next.js builds to a standard Node.js server (
next start) which Railway deploys as a Docker container without any special configuration. First-class Railway support. - Core Web Vitals targets: LCP < 2.5 s, CLS (Cumulative Layout Shift) < 0.1, INP (Interaction to Next Paint) < 200 ms. SSG (Static Site Generation: HTML is generated at build time and served from a CDN; ultra-fast but content is stale until you rebuild) pages served via CDN achieve LCP < 1.5 s. ISR (Incremental Static Regeneration: SSG with timed or on-demand regeneration of pages in the background, so content can update without a full rebuild) pages with 1-hour revalidation are served from Railway's edge until revalidated. SSR (Server-Side Rendering: HTML is generated on every request on the server; data is always fresh but costs more per request and has higher TTFB (Time to First Byte)) pages are cached at the Next.js layer between requests.
Considered Options
- Next.js with App Router (hybrid SSG/ISR/SSR/CSR — Client-Side Rendering: server sends a minimal shell and JS; the browser fetches data and renders UI; best for highly interactive SPA-like areas, weaker initial SEO) — single framework handling all rendering strategies and the BFF layer.
- React SPA (Vite) + separate Express BFF — full client-side rendering, separate backend for session management and API aggregation.
- Remix — server-rendered React framework with a different routing and data-loading model.
- Astro with React islands — static-first framework, excellent for editorial content but less suitable for interactive dashboard and comparison flows.
- Next.js Pages Router (hybrid, classic) — same framework but older Pages Router rather than App Router.
Decision Outcome
Option 1: Next.js 14 with App Router is adopted.
The rendering strategy is assigned per page type:
| Page Type | Strategy | Revalidation | Reason |
|---|---|---|---|
| Category hubs, filter pages, issuer pages | SSG + ISR | revalidate: 3600 |
High SEO value; CDN-cached; ISR refreshes rates hourly |
| Product detail pages | SSG + ISR | revalidate: 14400 |
Highest conversion value; pre-rendered for LCP; ISR on rate change |
| Editorial / guide pages | SSG | On-demand only | Content changes rarely; full CDN caching; no personalization |
| Comparison pages | SSR | Per-request | Dynamic product set; fresh data required; canonical control |
| User dashboard | CSR (React Server Components shell + client components) | — | Authenticated, personalized, not indexed |
| Admin UI | CSR | — | Internal, role-gated, not indexed |
The Next.js app also serves as the BFF through Next.js Route Handlers and Server Actions:
- Clerk session validation in Next.js middleware (runs on every request to protected routes).
- Server-side API aggregation: SSR and RSC pages call the Backend API Service over the internal Railway network, compose the page payload, and render HTML — the browser never calls the API directly.
- No raw tokens in the browser: Clerk SDK sets HttpOnly, Secure cookies. The browser never receives a JWT.
On-demand ISR revalidation is triggered by the Data Ingestion Service via POST /api/revalidate with an internal HMAC (Hash-based Message Authentication Code)-signed token after a successful product data write. This ensures that product detail and category pages reflect updated rates within seconds of ingestion completing, without waiting for the TTL (Time to Live)-based revalidation window.
Positive Consequences
- SEO-optimal: All public pages render to full HTML before reaching the browser. Google crawls them without JavaScript execution. Structured data (JSON-LD (JavaScript Object Notation for Linked Data)) is embedded in the
<head>at render time. - Core Web Vitals: SSG pages served from Railway's edge achieve LCP < 1.5 s. ISR pages revalidate in the background without blocking the user. SSR comparison pages achieve LCP < 2.5 s with Railway's internal API latency under 50 ms.
- Token security: Clerk tokens never touch the browser. HttpOnly cookies resist XSS (Cross-Site Scripting) token theft.
- Unified codebase: One TypeScript codebase, one Railway service for both the frontend and BFF. Reduces operational surface area.
- Railway compatibility:
next startis a standard Node.js server. Railway builds it with a Dockerfile and runs it as a web service — no special configuration. - ISR freshness: On-demand ISR means financial rates on product pages are updated within ~10 seconds of a successful ingestion run, not at an arbitrary TTL boundary.
Negative Consequences / Trade-offs
- Deployment complexity: SSG builds can be slow at large scale (many product detail pages). Mitigated by generating only the top-traffic pages at build time (
generateStaticParamslimited to the top 50 products per vertical) and relying on ISR for the long tail. - SSR memory footprint: SSR comparison pages consume server memory proportional to concurrent requests. Railway auto-scales vertically within the selected instance size. Horizontal scaling requires a Redis session store (Phase 2).
- App Router learning curve: Next.js App Router (RSC (React Server Components), Server Actions, streaming) is a newer paradigm with a steeper learning curve than the Pages Router. Mitigated by establishing clear conventions for when to use each pattern in the codebase README.
- No static export: Cannot use
next export(static HTML only) because ISR and SSR require a running server. This is acceptable since Railway hosts the Node.js server.
Pros and Cons of the Options
| Option | SEO | BFF Integration | Ops Overhead | Railway Fit |
|---|---|---|---|---|
| Next.js App Router (chosen) | Excellent | Native (RSC/Route Handlers) | Low (single service) | First-class |
| React SPA + Express BFF | Poor (CSR only) | Separate service | Medium (2 services) | Good |
| Remix | Excellent | Native (loaders) | Low | Good |
| Astro + React islands | Excellent | Limited | Low | Good |
| Next.js Pages Router | Excellent | Good (getServerSideProps) | Low | First-class |
Remix was the strongest alternative. Its loader pattern is cleaner than Next.js App Router's server components for data-heavy pages. However, Next.js has a larger ecosystem, better Clerk integration, and more Railway deploy examples. The App Router's RSC model aligns better with the planned progressive enhancement of the dashboard in Phase 2.
References
- Richards & Ford, Fundamentals of Software Architecture, Ch. 2 (Architectural Thinking, Trade-offs)
- NerdWallet reference architecture — ADR-001 (Hybrid SSR/SSG with Next.js), ADR-002 (BFF pattern)
- Next.js documentation: App Router, ISR, on-demand revalidation
- Architecture reference §3 (Container Diagram), §6 (SEO Architecture)