Skip to main content
The CGIAR Risk Intelligence frontend is a Next.js 15 application deployed as a static site to S3 with CloudFront CDN.

Prerequisites

  • AWS CLI configured with valid credentials
  • pnpm installed
  • jq installed
  • CloudFormation stack AllianceRiskStack already deployed

Quick Start

From the project root:
This runs scripts/deploy-web.sh, which handles the full deployment pipeline.

Deployment Pipeline

The deploy-web.sh script performs the following steps:

1. Fetch Stack Outputs

The deployment script queries CloudFormation stack outputs to get:
  • WebBucketName — S3 bucket for static files
  • CloudFrontDistributionId — CloudFront distribution ID for cache invalidation
  • ApiUrl — API Gateway endpoint URL
  • CognitoUserPoolId — Cognito User Pool ID
  • CognitoClientId — Cognito App Client ID

2. Set Build-Time Environment Variables

Next.js bakes NEXT_PUBLIC_* environment variables into the static build:
Important: These values are embedded in the JavaScript bundle at build time. If infrastructure changes (new Cognito pool, new API Gateway), you must rebuild and redeploy the frontend.

3. Build Static Export

The Next.js build produces a static export in packages/web/out/:
Configuration (packages/web/next.config.ts):
No Dynamic Routes: Static export requires all paths to be known at build time. The app uses query params (?id=xxx) instead of dynamic [id] routes.

4. Sync Static Assets to S3 (Immutable Cache)

Files with content-hashed filenames (JS, CSS, images) are uploaded with a 1-year cache:
Cache Strategy:
  • max-age=31536000 — 1 year (365 days)
  • immutable — Browser never revalidates (saves bandwidth)
Content Hashing: Next.js generates filenames like _next/static/chunks/app-123abc.js. When code changes, the hash changes, so old files are never served.

5. Sync HTML/JSON to S3 (No Cache)

HTML and JSON files are uploaded with no caching to always serve the latest version:
Cache Strategy:
  • max-age=0 — Don’t cache locally
  • must-revalidate — Always check with server
.txt Files: Next.js App Router generates .txt files for RSC (React Server Components) payloads. These are used for client-side navigation.

6. Create CloudFront Invalidation

CloudFront caches files at edge locations. After uploading new files, we invalidate the entire cache:
Propagation Time: 1-2 minutes for the invalidation to reach all edge locations. Cost: First 1,000 invalidation paths per month are free, then $0.005 per path. Using /* counts as 1 path.

CloudFront Configuration

URL Rewrite Function

Next.js static export generates .html files (/login.html, /dashboard.html), but users navigate to extensionless URLs (/login, /dashboard). A CloudFront Function rewrites requests:
Result:
  • User navigates to /login → CloudFront requests /login.html from S3
  • User navigates to /dashboard/ → CloudFront requests /dashboard.html from S3
  • User requests /_next/static/chunks/app-123abc.js → No rewrite (has extension)

Error Responses (SPA Routing)

CloudFront is configured to serve index.html for 403/404 errors:
Why? When a user navigates directly to a client-side route (e.g., /dashboard?id=123), CloudFront doesn’t find /dashboard?id=123.html in S3 and returns 404. We intercept this and serve index.html, which boots the React app and handles the route. Error Caching: ErrorCachingMinTTL: 0 ensures 404s are not cached (important during development).

Build-Time Environment Variables

These variables are baked into the JavaScript bundle: Example (packages/web/src/lib/api-client.ts):

Deployment Order

When deploying from scratch:
For routine code changes (no infrastructure changes), only step 4 is needed. If infrastructure changes (new Cognito pool, new API Gateway), you must:

Troubleshooting

Build did not produce out/ directory

Cause: next.config.ts is missing output: 'export' Solution: Ensure next.config.ts has:

CloudFront serves old content after deployment

Cause: Invalidation not yet propagated (takes 1-2 minutes) Solution: Wait 1-2 minutes, or check invalidation status:

404 on client-side routes

Cause: CloudFront error responses not configured Solution: Ensure the CloudFormation template has CustomErrorResponses for 403/404 → /index.html

API calls fail with CORS errors

Cause: NEXT_PUBLIC_API_URL not set or incorrect Solution: Verify the build logs show the correct API URL:

Authentication fails (Cognito errors)

Cause: NEXT_PUBLIC_COGNITO_USER_POOL_ID or NEXT_PUBLIC_COGNITO_CLIENT_ID not set or incorrect Solution: Verify the build logs show the correct Cognito IDs:

.txt files return 404 in browser (but work in Next.js)

Expected behavior. The CloudFront Function blocks direct browser navigation to .txt files and serves .html instead. Next.js client-side navigation includes the RSC: 1 header, so .txt files work correctly during navigation.

Performance Optimization

  • CloudFront CDN: Global edge locations reduce latency
  • Gzip Compression: Enabled by default on CloudFront
  • Immutable Cache: 1-year cache for hashed assets (JS, CSS, images)
  • HTTP/2: Enabled for multiplexing
  • Price Class 100: Cheaper edge locations (North America + Europe)

Security Best Practices

  • HTTPS Only: CloudFront redirects HTTP → HTTPS
  • S3 Private: Bucket accessible only via CloudFront OAI (Origin Access Identity)
  • No Secrets in Frontend: All sensitive data (DB credentials, API keys) lives in the backend
  • CORS: API Gateway configured to allow requests from CloudFront origin

Cache Strategy Summary

Manual S3 Sync (for debugging)

If you need to manually sync files without the deployment script:

Rollback

To rollback to a previous deployment, you must rebuild and redeploy from a previous git commit:
Consider maintaining separate branches for each environment (dev, staging, production) for easier rollback.