diff --git a/README.md b/README.md index 66fc26d..936ae84 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,17 @@ REP is a formal, open specification — not just a tool. --- +## Examples + +| Example | Pattern | Description | +|---|---|---| +| [`examples/todo-react/`](examples/todo-react/) | React + embedded | Full React todo app — `useRep()`, `useRepSecure()`, hot reload, `FROM scratch` Docker image | +| [`examples/simple-html/`](examples/simple-html/) | Plain HTML + embedded | Single HTML file, SDK via esm.sh, no build step, `FROM scratch` image | +| [`examples/nextjs-proxy/`](examples/nextjs-proxy/) | Next.js SSR + proxy | Next.js server behind the gateway in proxy mode; Docker Compose two-service setup | +| [`examples/nextjs-csr-embedded/`](examples/nextjs-csr-embedded/) | Next.js CSR + embedded + Kubernetes | Static export served by gateway; ConfigMap-driven feature flags with hot-reload variant (zero pod restarts) | + +--- + ## Contributing We welcome contributions. See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, commit conventions, and the release process. diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index cfc611f..a739118 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -144,6 +144,12 @@ export default defineConfig({ slug: 'examples/quick-no-manifest', }, { label: 'Todo App (React)', slug: 'examples/todo-react' }, + { label: 'Simple HTML (ESM.sh)', slug: 'examples/simple-html' }, + { label: 'Next.js — Proxy Mode', slug: 'examples/nextjs-proxy' }, + { + label: 'Next.js CSR + Kubernetes', + slug: 'examples/nextjs-csr-kubernetes', + }, ], }, { diff --git a/docs/src/content/docs/examples/nextjs-csr-kubernetes.mdx b/docs/src/content/docs/examples/nextjs-csr-kubernetes.mdx new file mode 100644 index 0000000..f37cc01 --- /dev/null +++ b/docs/src/content/docs/examples/nextjs-csr-kubernetes.mdx @@ -0,0 +1,285 @@ +--- +title: Next.js CSR — Embedded Mode + Kubernetes +description: Export Next.js as a static site, serve it with the REP gateway in embedded mode, and manage feature flags dynamically via a Kubernetes ConfigMap. Includes a hot-reload variant with zero pod restarts. +--- + +import { Aside, FileTree, Steps } from '@astrojs/starlight/components'; + +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/`](https://github.com/ruachtech/rep/tree/main/examples/nextjs-csr-embedded). + +## Architecture + +``` +Browser → REP Gateway :8080 (embedded mode) + │ serves /static (Next.js export) + │ injects __rep__ payload from environment vars + └ environment vars come from Kubernetes ConfigMap + Secret +``` + +No upstream server. No Node.js runtime in production. The final container is `FROM scratch`. + +## 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 + + +- examples/nextjs-csr-embedded/ + - src/ + - app/ + - layout.tsx + - page.tsx + - components/ + - ConfigPanel.tsx Reads REP public + sensitive vars + - FeatureFlags.tsx Hot-reload-aware flag toggle display + - k8s/ + - 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 + +Set `output: 'export'` in `next.config.ts`: + +```typescript +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 + +```tsx +'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 (/* … */); +} +``` + +```tsx +'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 ( + + ); +} +``` + +## Dockerfile (FROM scratch) + +Three stages: download gateway, build Next.js, assemble scratch image: + +```dockerfile +FROM alpine:3.21 AS gateway +ARG GATEWAY_VERSION=0.1.3 +RUN 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 builder +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci +COPY . . +RUN npm run build # produces out/ + +FROM scratch +COPY --from=gateway /rep-gateway /rep-gateway +COPY --from=builder /app/out /static +COPY --from=gateway /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +USER 65534:65534 +EXPOSE 8080 +ENTRYPOINT ["/rep-gateway", "--mode", "embedded", "--static-dir", "/static"] +``` + +## Kubernetes: ConfigMap for feature flags + +### Standard pattern (envFrom) + +Store all `REP_PUBLIC_*` vars in a ConfigMap. Sensitive vars go in a Secret: + +```yaml +# configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: rep-config +data: + 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" +``` + +```yaml +# secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: rep-secrets +stringData: + REP_SENSITIVE_ANALYTICS_KEY: "ak_live_replace_me" +``` + +Reference both in the Deployment: + +```yaml +envFrom: + - configMapRef: + name: rep-config + - secretRef: + name: rep-secrets +``` + +To update feature flags: + +```bash +kubectl edit configmap rep-config +# Change REP_PUBLIC_FEATURE_FLAGS, save + +kubectl rollout restart deployment/frontend +# New pods start with updated config +``` + +### 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`: + + +1. **Apply the env-file ConfigMap** + + ```bash + kubectl apply -f k8s/configmap-envfile.yaml + ``` + + The ConfigMap stores a `.env` file as a single key: + + ```yaml + data: + rep.env: | + REP_PUBLIC_API_URL=https://api.example.com + REP_PUBLIC_FEATURE_FLAGS=dark-mode,new-checkout + ``` + +2. **Mount the file in the Deployment** + + ```yaml + 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-volume + mountPath: /config + readOnly: true + volumes: + - name: rep-config-volume + configMap: + name: rep-config-envfile + ``` + +3. **Update feature flags** + + ```bash + kubectl edit configmap rep-config-envfile + # Change REP_PUBLIC_FEATURE_FLAGS in the rep.env block, save + ``` + +4. **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). + +5. **Components re-render automatically** + + Any component using `useRep('FEATURE_FLAGS')` re-renders with the new value. No pod restart, no page refresh. + + + + +## Running locally + +### Docker + +```bash +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-csr +``` + +### CLI dev mode + +```bash +cd examples/nextjs-csr-embedded + +# Build the static export +npm run build # → out/ + +# Serve with gateway in embedded mode +npx @rep-protocol/cli dev \ + --mode embedded \ + --static-dir ./out \ + --env-file .env.local +``` + +## 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](/examples/nextjs-proxy/) when you do. diff --git a/docs/src/content/docs/examples/nextjs-proxy.mdx b/docs/src/content/docs/examples/nextjs-proxy.mdx new file mode 100644 index 0000000..630a2fc --- /dev/null +++ b/docs/src/content/docs/examples/nextjs-proxy.mdx @@ -0,0 +1,205 @@ +--- +title: Next.js — Proxy Mode +description: Run the REP gateway as a reverse proxy in front of a Next.js SSR server. Environment variables are injected into every HTML response without touching the Next.js build. +--- + +import { Aside, FileTree, Tabs, TabItem } from '@astrojs/starlight/components'; + +The REP gateway runs in **proxy mode** in front of a Next.js server. It intercepts every HTML response and injects the `__rep__` payload — no changes to the Next.js build pipeline required. + +Source code at [`examples/nextjs-proxy/`](https://github.com/ruachtech/rep/tree/main/examples/nextjs-proxy). + +## Architecture + +``` +Client → REP Gateway :8080 (proxy mode) + ↓ injects __rep__ payload into HTML responses + Next.js Server :3000 +``` + +The Next.js server is never exposed directly. All traffic flows through the gateway. Config lives entirely in the gateway's environment variables — the Next.js image is the same across every deployment. + +## When to use this pattern + +- Next.js App Router or Pages Router with server-side rendering (SSR) +- You want the full Next.js server (streaming, server actions, API routes) +- Docker Compose, Kubernetes single-container pod, or multi-service deployments + +## File structure + + +- examples/nextjs-proxy/ + - src/ + - app/ + - layout.tsx + - page.tsx Server component shell + - components/ + - EnvDisplay.tsx Client component — reads REP vars + - IntegrityBanner.tsx Verifies payload was not modified in transit + - Dockerfile Next.js-only image (for docker-compose) + - Dockerfile.single Gateway + Next.js in one image (for Kubernetes) + - docker-compose.yml Two-service stack (recommended) + - docker-entrypoint.sh Startup script for Dockerfile.single + - next.config.ts + - .rep.yaml + - .env.example + + +## Reading REP vars in Next.js + +REP runs entirely in the browser. In the App Router, use `'use client'` components: + +```tsx +'use client'; + +import { useRep, useRepSecure } from '@rep-protocol/react'; + +export function EnvDisplay() { + // Synchronous — available on first render, no loading state + const apiUrl = useRep('API_URL', 'http://localhost:3001'); + const envName = useRep('ENV_NAME', 'development'); + const features = useRep('FEATURE_FLAGS', ''); + + // Async — decrypted via the session-key endpoint + const { value: analyticsKey, loading } = useRepSecure('ANALYTICS_KEY'); + + return ( +
+

Environment: {envName}

+

API: {apiUrl}

+

Key: {loading ? 'loading…' : analyticsKey}

+
+ ); +} +``` + + + +## Integrity verification + +The example includes an `IntegrityBanner` client component that verifies the payload was not modified after the gateway injected it. The SDK checks a SHA-256 SRI hash embedded on the ` + ↓ serves index.html +``` + +The HTML file imports the SDK as an ES module from esm.sh — no npm install, no bundler: + +```html + +``` + +