Skip to main content

Amplify deploy

Status: Scaffolded 2026-09-24

The where and when of web deployment is in web app deployment. This page is the repo side: the build spec and the headers the app ships with.

amplify.yml​

version: 1
frontend:
phases:
preBuild:
commands:
- nvm use 24
- npm ci
- env | grep -E '^NEXT_PUBLIC_' >> .env.production
build:
commands:
- npm run build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- node_modules/**/*
- .next/cache/**/*
  • nvm use 24: the Amplify image ships several Node versions; this matches .nvmrc.
  • npm ci: exact lockfile install. A lockfile drift fails here, which is the point.
  • env | grep -E '^NEXT_PUBLIC_' >> .env.production: the allowlist. Amplify console variables are present in the build shell but not in the SSR runtime; Next reads .env.production at build and inlines NEXT_PUBLIC_* into both bundles. Only that prefix passes, so a server-only value set in the console cannot reach the client. There are no server-only values in this app today, and the grep keeps it that way.
  • npm run build: next build --turbopack. validate runs the same command, so a build that passes CI passes Amplify.
  • baseDirectory: .next: WEB_COMPUTE reads the standalone output from here.
  • cache: modules and the Next compiler cache, keyed by Amplify per branch.

Security headers​

next.config.ts returns these for every route from headers():

HeaderValue
Strict-Transport-Securitymax-age=63072000; includeSubDomains; preload
X-Content-Type-Optionsnosniff
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=(self), payment=(), usb=()
X-Frame-OptionsDENY, redundant with the CSP frame-ancestors for older browsers
Content-Security-Policybelow

geolocation=(self) because the places page asks the browser for a position to center the map; the value stays in the page and goes nowhere.

Content security policy​

default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https://tiles.openfreemap.org <media CDN>;
connect-src 'self' https://api.vegangrove.org wss://api.vegangrove.org https://tiles.openfreemap.org;
media-src 'self' blob: https://*.b-cdn.net;
frame-src https://www.youtube-nocookie.com;
worker-src 'self' blob:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;

Origins, and why each is there:

  • api.vegangrove.org (https and wss): the API and the Socket.IO /messages namespace.
  • tiles.openfreemap.org: the map style, glyphs, sprites, and vector tiles. MapLibre fetches them with fetch and loads sprites as images, hence both connect-src and img-src.
  • *.b-cdn.net: Bunny Stream delivers HLS from its CDN. If the Bunny iframe player is used instead of a native <video> with HLS, its player origin is added to frame-src.
  • www.youtube-nocookie.com: media-library trailers, click to load, never autoplayed, never on the page until tapped.
  • the media CDN: the CloudFront distribution in front of the S3 image bucket. Its hostname is set from NEXT_PUBLIC_MEDIA_BASE_URL at build so the policy does not hard-code a distribution.
  • blob: and worker-src: MapLibre runs its tile workers from blob URLs.

'unsafe-inline' on scripts is the one weak spot: Next's hydration and the theme script are inline. Moving to a nonce-based policy set in middleware.ts is on the roadmap. No third-party script, font, or analytics origin appears anywhere in the policy, and adding one is a PR that must name the ADR it is overriding.