Next.js CSR — Embedded Mode + Kubernetes
Next.js with output: 'export' produces a fully static site. The REP gateway serves the output in embedded mode — no Node.js server needed in production. A Kubernetes ConfigMap holds all runtime config. Update it to change feature flags across all pods without rebuilding the image.
Source code at examples/nextjs-csr-embedded/.
Architecture
Section titled “Architecture”Browser → REP Gateway :8080 (embedded mode) │ serves /static (Next.js export) │ injects __rep__ payload from environment vars └ environment vars come from Kubernetes ConfigMap + SecretNo upstream server. No Node.js runtime in production. The final container is FROM scratch.
When to use this pattern
Section titled “When to use this pattern”- Next.js app with no server-side rendering requirements (pure CSR)
- Kubernetes deployments where you want to change feature flags without rebuilds
- Minimal container surface area (
FROM scratch, ~8MB total image size)
File structure
Section titled “File structure”Directoryexamples/nextjs-csr-embedded/
Directorysrc/
Directoryapp/
- layout.tsx
- page.tsx
Directorycomponents/
- ConfigPanel.tsx Reads REP public + sensitive vars
- FeatureFlags.tsx Hot-reload-aware flag toggle display
Directoryk8s/
- configmap.yaml Standard envFrom pattern
- configmap-envfile.yaml Hot-reload variant (mounted as file)
- deployment.yaml Both variants documented
- secret.yaml Sensitive vars
- service.yaml + Ingress
- Dockerfile 3-stage: gateway + Next.js build + FROM scratch
- next.config.ts output: ‘export’
- .rep.yaml
- .env.example
Next.js static export
Section titled “Next.js static export”Set output: 'export' in next.config.ts:
const nextConfig: NextConfig = { output: 'export', images: { unoptimized: true }, // required for static export};The build produces an out/ directory of static HTML/CSS/JS. This is what the gateway serves.
Reading REP vars in client components
Section titled “Reading REP vars in client components”'use client';
import { useRep, useRepSecure } from '@rep-protocol/react';
export function ConfigPanel() { // Synchronous — available on first render const apiUrl = useRep('API_URL', 'http://localhost:3001'); const envName = useRep('ENV_NAME', 'development');
// Decrypted on demand (async) const { value: analyticsKey, loading } = useRepSecure('ANALYTICS_KEY');
return (/* … */);}'use client';
import { useRep } from '@rep-protocol/react';
export function FeatureFlags() { // useRep() subscribes to hot reload — component re-renders // when FEATURE_FLAGS changes without a page refresh const rawFlags = useRep('FEATURE_FLAGS', ''); const active = new Set(rawFlags.split(',').map(f => f.trim()).filter(Boolean));
return ( <ul> {['dark-mode', 'new-checkout', 'ai-assist'].map(flag => ( <li key={flag}>{active.has(flag) ? '✅' : '⬜'} {flag}</li> ))} </ul> );}Dockerfile (FROM scratch)
Section titled “Dockerfile (FROM scratch)”Three stages: download gateway, build Next.js, assemble scratch image:
FROM alpine:3.21 AS gatewayARG GATEWAY_VERSION=0.1.3RUN apk add --no-cache curl ca-certificates && \ curl -fsSL "…/rep-gateway_${GATEWAY_VERSION}_linux_amd64.tar.gz" \ | tar -xz -C /tmp && \ mv /tmp/rep-gateway /rep-gateway && chmod +x /rep-gateway
FROM node:22-alpine AS builderWORKDIR /appCOPY package.json package-lock.json* ./RUN npm ciCOPY . .RUN npm run build # produces out/
FROM scratchCOPY --from=gateway /rep-gateway /rep-gatewayCOPY --from=builder /app/out /staticCOPY --from=gateway /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/USER 65534:65534EXPOSE 8080ENTRYPOINT ["/rep-gateway", "--mode", "embedded", "--static-dir", "/static"]Kubernetes: ConfigMap for feature flags
Section titled “Kubernetes: ConfigMap for feature flags”Standard pattern (envFrom)
Section titled “Standard pattern (envFrom)”Store all REP_PUBLIC_* vars in a ConfigMap. Sensitive vars go in a Secret:
apiVersion: v1kind: ConfigMapmetadata: name: rep-configdata: REP_PUBLIC_API_URL: "https://api.example.com" REP_PUBLIC_ENV_NAME: "production" REP_PUBLIC_FEATURE_FLAGS: "dark-mode,new-checkout" # ← update this to toggle flags REP_GATEWAY_MODE: "embedded" REP_GATEWAY_HOT_RELOAD: "true"apiVersion: v1kind: Secretmetadata: name: rep-secretsstringData: REP_SENSITIVE_ANALYTICS_KEY: "ak_live_replace_me"Reference both in the Deployment:
envFrom: - configMapRef: name: rep-config - secretRef: name: rep-secretsTo update feature flags:
kubectl edit configmap rep-config# Change REP_PUBLIC_FEATURE_FLAGS, save
kubectl rollout restart deployment/frontend# New pods start with updated configHot-reload variant (zero pod restarts)
Section titled “Hot-reload variant (zero pod restarts)”Mount the ConfigMap as a .env file and start the gateway with --hot-reload --hot-reload-mode=file_watch:
-
Apply the env-file ConfigMap
Terminal window kubectl apply -f k8s/configmap-envfile.yamlThe ConfigMap stores a
.envfile as a single key:data:rep.env: |REP_PUBLIC_API_URL=https://api.example.comREP_PUBLIC_FEATURE_FLAGS=dark-mode,new-checkout -
Mount the file in the Deployment
args:- "--mode=embedded"- "--static-dir=/static"- "--env-file=/config/rep.env"- "--hot-reload"- "--hot-reload-mode=file_watch"- "--watch-path=/config/rep.env"volumeMounts:- name: rep-config-volumemountPath: /configreadOnly: truevolumes:- name: rep-config-volumeconfigMap:name: rep-config-envfile -
Update feature flags
Terminal window kubectl edit configmap rep-config-envfile# Change REP_PUBLIC_FEATURE_FLAGS in the rep.env block, save -
Wait ~60 seconds
Kubernetes propagates ConfigMap changes to mounted files. The gateway detects the file change, re-reads vars, re-encrypts sensitive values with a fresh ephemeral key, and pushes a reload event to all connected browsers via
/rep/changes(SSE). -
Components re-render automatically
Any component using
useRep('FEATURE_FLAGS')re-renders with the new value. No pod restart, no page refresh.
Running locally
Section titled “Running locally”Docker
Section titled “Docker”cd examples/nextjs-csr-embedded
docker build -t rep-nextjs-csr .
docker run --rm -p 8080:8080 \ -e REP_PUBLIC_API_URL=https://api.example.com \ -e REP_PUBLIC_ENV_NAME=production \ -e REP_PUBLIC_FEATURE_FLAGS=dark-mode,new-checkout \ -e REP_SENSITIVE_ANALYTICS_KEY=ak_live_abc123 \ rep-nextjs-csrCLI dev mode
Section titled “CLI dev mode”cd examples/nextjs-csr-embedded
# Build the static exportnpm run build # → out/
# Serve with gateway in embedded modenpx @rep-protocol/cli dev \ --mode embedded \ --static-dir ./out \ --env-file .env.localComparison: static vs SSR
Section titled “Comparison: static vs SSR”| CSR + Embedded (this example) | SSR + Proxy | |
|---|---|---|
Next.js output | 'export' (static) | default (server) |
| Runtime container | FROM scratch (~8MB) | Node.js image (~150MB) |
| Server features | ❌ No API routes, no SSR | ✅ Full Next.js server |
| Feature flag updates | ConfigMap edit + SIGHUP (no restart) | ConfigMap edit + rolling restart |
| Gateway mode | embedded | proxy |
Use the CSR + embedded pattern when you don’t need server-side rendering or Next.js API routes. Use the proxy mode example when you do.