Build once. Configure anywhere. Securely.
REP is an open protocol for injecting environment variables into browser apps at container runtime — not build time. It gives you security classification, encryption, integrity verification, and hot reload, with zero build-tool coupling.
npm install @rep-protocol/sdk- const apiUrl = import.meta.env.VITE_API_URL;
+ import { rep } from '@rep-protocol/sdk';
+ const apiUrl = rep.get('API_URL'); // synchronous, no loading stateFROM node:22-alpine AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY --from=ghcr.io/ruachtech/rep/gateway:latest /usr/local/bin/rep-gateway /usr/local/bin/rep-gateway
ENTRYPOINT ["rep-gateway"]
CMD ["--upstream", "nginx", "--port", "8080"]docker run -p 8080:8080 \
-e REP_PUBLIC_API_URL=https://api.example.com \
-e REP_PUBLIC_FEATURE_FLAGS=dark-mode,beta \
-e REP_SENSITIVE_ANALYTICS_KEY=UA-12345-1 \
myapp:latestSame image, different config per environment. Done.
All packages are published to npm under the @rep-protocol scope.
| Package | Install | Description |
|---|---|---|
@rep-protocol/sdk |
npm i @rep-protocol/sdk |
Core SDK — zero dependencies, ~1.5KB gzipped |
@rep-protocol/react |
npm i @rep-protocol/react |
React hooks — useRep(), useRepSecure() |
@rep-protocol/vue |
npm i @rep-protocol/vue |
Vue composables — useRep(), useRepSecure() |
@rep-protocol/svelte |
npm i @rep-protocol/svelte |
Svelte stores — repStore(), repSecureStore() |
@rep-protocol/cli |
npm i -D @rep-protocol/cli |
CLI — rep dev, rep lint, rep validate, rep typegen |
@rep-protocol/codemod |
npx @rep-protocol/codemod |
Auto-migrate from Vite, CRA, or Next.js env patterns |
import { rep } from '@rep-protocol/sdk';
// PUBLIC vars — synchronous, no async, no loading state
const apiUrl = rep.get('API_URL');
const flags = rep.get('FEATURE_FLAGS');
// SENSITIVE vars — encrypted, decrypted on demand
const key = await rep.getSecure('ANALYTICS_KEY');
// Hot reload — react to config changes without page refresh
rep.onChange('FEATURE_FLAGS', (newValue, oldValue) => {
console.log(`Flags changed: ${oldValue} → ${newValue}`);
});npm install @rep-protocol/sdk @rep-protocol/reactimport { useRep, useRepSecure } from '@rep-protocol/react';
function App() {
const apiUrl = useRep('API_URL');
const flags = useRep('FEATURE_FLAGS', 'defaults');
const { value: analyticsKey, loading } = useRepSecure('ANALYTICS_KEY');
// Auto re-renders on hot reload config changes
return <div>API: {apiUrl}</div>;
}npm install @rep-protocol/sdk @rep-protocol/vue<script setup>
import { useRep, useRepSecure } from '@rep-protocol/vue';
const apiUrl = useRep('API_URL');
const analyticsKey = useRepSecure('ANALYTICS_KEY');
</script>
<template>
<div>API: {{ apiUrl }}</div>
</template>npm install @rep-protocol/sdk @rep-protocol/svelte<script>
import { repStore, repSecureStore } from '@rep-protocol/svelte';
const apiUrl = repStore('API_URL');
const analyticsKey = repSecureStore('ANALYTICS_KEY');
</script>
<div>API: {$apiUrl}</div>REP uses a prefix convention to classify variables into security tiers:
| Prefix | Tier | Behaviour |
|---|---|---|
REP_PUBLIC_* |
PUBLIC | Plaintext in page source. Synchronous access via rep.get(). |
REP_SENSITIVE_* |
SENSITIVE | AES-256-GCM encrypted. Async access via rep.getSecure(). |
REP_SERVER_* |
SERVER | Never sent to the browser. Gateway-only. |
Prefixes are stripped in code: REP_PUBLIC_API_URL becomes rep.get('API_URL').
Migrate an existing project automatically with the codemod:
# Vite project
npx @rep-protocol/codemod --framework vite --src ./src
# Create React App
npx @rep-protocol/codemod --framework cra --src ./src
# Next.js
npx @rep-protocol/codemod --framework next --src ./srcThis transforms import.meta.env.VITE_* / process.env.REACT_APP_* / process.env.NEXT_PUBLIC_* calls into rep.get() calls and adds the SDK import.
The CLI bundles a platform-specific gateway binary and provides development tools:
npm install -D @rep-protocol/cli
# Local dev server (wraps the gateway)
npx rep dev --env-file .env
# Scan your built bundle for leaked secrets
npx rep lint ./dist
# Validate your .rep.yaml manifest
npx rep validate
# Generate TypeScript types from your manifest
npx rep typegenservices:
frontend-staging:
image: myapp:latest
environment:
REP_PUBLIC_API_URL: "https://api.staging.example.com"
REP_PUBLIC_FEATURE_FLAGS: "dark-mode,beta-checkout"
REP_SENSITIVE_ANALYTICS_KEY: "UA-XXXXX-staging"
REP_SERVER_INTERNAL_SECRET: "never-reaches-browser"
frontend-prod:
image: myapp:latest # SAME IMAGE
environment:
REP_PUBLIC_API_URL: "https://api.example.com"
REP_PUBLIC_FEATURE_FLAGS: "dark-mode"
REP_SENSITIVE_ANALYTICS_KEY: "UA-XXXXX-prod"
REP_SERVER_INTERNAL_SECRET: "also-never-reaches-browser"The gateway is a static Go binary with zero dependencies. You can run it in a scratch container:
FROM node:22-alpine AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM scratch
COPY --from=ghcr.io/ruachtech/rep/gateway:latest /usr/local/bin/rep-gateway /rep-gateway
COPY --from=build /app/dist /static
USER 65534:65534
EXPOSE 8080
ENTRYPOINT ["/rep-gateway", "--mode", "embedded", "--static-dir", "/static"]- Proxy mode (default): Reverse proxy to an upstream (nginx, caddy). Injects config into proxied HTML responses.
- Embedded mode: Serves static files directly. No upstream needed. Enables
FROM scratchcontainers.
Every frontend framework resolves environment variables at build time via static string replacement. The resulting bundle is plain JS/HTML/CSS — there is no process object, no runtime. The browser has no concept of environment variables.
This means:
- One image per environment. You build
app:staging,app:prod,app:dev— each with baked-in config. - Broken CI/CD promotion. The image that passed your tests is a different binary than what goes to prod.
- Config changes require redeployment. Changed an API URL? Rebuild and redeploy.
- No security model. Every workaround dumps all env vars as plaintext into
window.__ENV__.
| Approach | Limitation |
|---|---|
envsubst / sed on JS bundles |
Fragile string replacement on minified code. Mutates container filesystem. |
Fetch /config.json at startup |
Network dependency, loading delay, race conditions. |
window.__ENV__ via shell script |
No standard, no security model, requires Node.js or bash in prod container. |
| Build-tool plugins | Couples your build pipeline. Framework-specific. No security. |
REP has a security-first design:
- AES-256-GCM encryption for sensitive variables
- HMAC-SHA256 integrity verification + SRI hashing on every payload
- Automatic secret detection — Shannon entropy analysis + known key format matching (AWS keys, JWTs, GitHub tokens, etc.)
- Single-use session keys with 30-second TTL and rate limiting
- Ephemeral keys generated at startup, never persisted to disk
--strictmode turns secret detection warnings into hard failures
Full threat model: spec/SECURITY-MODEL.md
REP is a formal, open specification — not just a tool.
| Document | Status |
|---|---|
| REP-RFC-0001 | Active |
| Security Model | Active |
| Integration Guide | Active |
| Example | Pattern | Description |
|---|---|---|
examples/todo-react/ |
React + embedded | Full React todo app — useRep(), useRepSecure(), hot reload, FROM scratch Docker image |
examples/simple-html/ |
Plain HTML + embedded | Single HTML file, SDK via esm.sh, no build step, FROM scratch image |
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/ |
Next.js CSR + embedded + Kubernetes | Static export served by gateway; ConfigMap-driven feature flags with hot-reload variant (zero pod restarts) |
We welcome contributions. See CONTRIBUTING.md for development setup, commit conventions, and the release process.
git clone https://github.com/ruachtech/rep.git
cd rep
pnpm install # requires pnpm >= 9.0.0
pnpm -r build # build all packages
pnpm -r test # test all packages
# Gateway (requires Go >= 1.24.5)
cd gateway && make testSpecification: CC BY 4.0. Code: Apache 2.0.
REP is a proposal by Ruach Tech. Built to solve a problem the entire frontend ecosystem has accepted for too long.