@alvarorestrepo/envshare-sdk
v0.2.0
Published
EnvShare SDK — Runtime environment variable injection
Maintainers
Readme
@alvarorestrepo/envshare-sdk
Runtime environment variable injection for Node.js — no .env files on disk.
The Problem
Every team has been here:
.envfiles get committed by accident — onegit add .and your production database credentials are in the repo history forever.- Sharing secrets over Slack or email — "hey can you send me the staging API key?" Copy-paste. Screenshot. Forwarded. Zero control.
- No audit trail — who accessed the production secrets? When? From where? Nobody knows.
- Variables drift out of sync — one developer updates a key locally, forgets to tell the team. Three hours of debugging later: "oh, the API key rotated."
- No per-environment access control — the intern has the same production credentials as the lead engineer.
.env files were designed for local convenience. They were never meant to be a secrets management system.
The Solution — How EnvShare Works
EnvShare replaces .env files with a centralized, encrypted, access-controlled platform. The SDK fetches your variables at runtime and injects them directly into process.env — nothing is ever written to disk.
┌─────────────────────────────────────────────────────────┐
│ EnvShare Platform │
│ envshared.vercel.app │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Dev │ │ Staging │ │ Prod │ ← Environments │
│ │ 12 vars │ │ 12 vars │ │ 12 vars │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ┌────┴────────────┴────────────┴────┐ │
│ │ AES-256-GCM Encryption │ │
│ │ Per-project derived keys │ │
│ └────────────────┬──────────────────┘ │
│ │ │
│ ┌────────────────┴──────────────────┐ │
│ │ REST API + Auth │ │
│ │ API Key + IP Allowlist + Audit │ │
│ └────────────────┬──────────────────┘ │
└───────────────────┼──────────────────────────────────────┘
│
┌─────────┴─────────┐
│ @alvarorestrepo/ │
│ envshare-sdk │
│ │
│ • init() │
│ • ETag caching │
│ • Auto-retry │
│ • Stale fallback │
└─────────┬─────────┘
│
▼
┌──────────────┐
│ Your App │
│ │
│ process.env │
│ .DATABASE_URL│
│ .API_SECRET │
│ .REDIS_URL │
└──────────────┘Your app calls init() once at startup. The SDK authenticates with your API key, verifies your IP is in the allowlist, decrypts the variables server-side, and injects them into process.env. From that point on, your code reads process.env.DATABASE_URL exactly like it would with a .env file — except the secret was never on disk.
⚠️ Prerequisites — Read This First
Before your SDK calls will work, you MUST configure both an API key and the IP allowlist on the platform.
EnvShare uses a two-layer security model. Both layers must pass for the SDK to receive variables:
- API Key — validates you have access to the project
- IP Allowlist — validates your machine/server has access to the specific environment
Setup Steps
- Go to envshared.vercel.app and create an account
- Create a project (e.g.,
my-app) - Add your environments (development, staging, production)
- Add your environment variables — they are encrypted with AES-256-GCM before storage
- Go to Settings → API Keys → Generate a key (copy it — it's shown only once)
- Go to Settings → IP Configuration → Add the IPs of your machines/servers:
- Local development: your public IP — find it at ifconfig.me
- CI/CD runners: your provider's IP ranges (e.g., GitHub Actions IP ranges)
- Production servers: your server's static IP or load balancer IP
- Wait for IP approval — if your role is Developer, an Owner or Admin must approve your IP request
- Now your SDK calls will work
How the Two Layers Work
Your Machine (IP: 203.0.113.42)
│
▼
┌──────────────┐
│ API Key │──→ "Do you have access to this PROJECT?"
│ Validation │ ✅ Valid key, not revoked
└──────┬───────┘
│
▼
┌──────────────┐
│ IP Allowlist │──→ "Is your IP approved for this ENVIRONMENT?"
│ Check │ ✅ 203.0.113.42 is in the allowlist
└──────┬───────┘
│
▼
┌──────────────┐
│ Decrypt & │──→ Variables decrypted with AES-256-GCM
│ Respond │ Returned to your app
└──────────────┘Important: If the IP allowlist for an environment is empty, access is DENIED — not open. This is by design. You must explicitly add at least one IP to access any environment.
Common Errors During Setup
| Error | Meaning | Fix |
|-------|---------|-----|
| AuthError (401) | API key is invalid or revoked | Generate a new key in Settings → API Keys |
| ForbiddenError (403) | Your IP is not in the allowlist | Add your IP in Settings → IP Configuration |
| NotFoundError (404) | Project or environment doesn't exist | Check spelling — slugs are case-sensitive |
Quick Start
Installation
npm install @alvarorestrepo/envshare-sdkOption 1: Programmatic (recommended)
import { init } from '@alvarorestrepo/envshare-sdk';
await init({
apiKey: process.env.ENVSHARE_API_KEY!,
project: 'my-app',
environment: 'production',
});
// process.env now has all your variables
console.log(process.env.DATABASE_URL);
console.log(process.env.API_SECRET);Option 2: Preload (zero code changes)
Set the required environment variables:
export ENVSHARE_API_KEY=es_live_xxxxx
export ENVSHARE_PROJECT=my-app
export ENVSHARE_ENVIRONMENT=productionRun your app with --import:
node --import @alvarorestrepo/envshare-sdk/preload app.jsVariables are fetched and injected into process.env before your application code runs.
Configuration Reference
| Option | Type | Default | Env Var Fallback | Description |
|--------|------|---------|------------------|-------------|
| apiKey | string | — | ENVSHARE_API_KEY | API key (starts with es_live_) |
| project | string | — | ENVSHARE_PROJECT | Project slug |
| environment | string | — | ENVSHARE_ENVIRONMENT | Environment name (development, staging, production) |
| apiUrl | string | https://envshared.vercel.app | ENVSHARE_API_URL | Base URL of the EnvShare API |
| cacheTtl | number | 300_000 (5 min) | — | Cache TTL in milliseconds |
| maxStale | number | 3_600_000 (1 hr) | — | Stale cache window in milliseconds |
| timeout | number | 10_000 (10s) | — | Request timeout in milliseconds |
| maxRetries | number | 3 | — | Max retry attempts for retryable errors |
| inject | boolean | true | — | Inject variables into process.env |
| logger | Logger | noopLogger | — | Logger instance (silent by default) |
Full example:
import { init } from '@alvarorestrepo/envshare-sdk';
await init({
apiKey: 'es_live_xxxxx',
project: 'my-app',
environment: 'production',
apiUrl: 'https://envshared.vercel.app',
cacheTtl: 300_000,
maxStale: 3_600_000,
timeout: 10_000,
maxRetries: 3,
inject: true,
});API Reference
init(config): Promise<FetchResult>
Initialize the SDK, fetch variables from the EnvShare API, and optionally inject them into process.env.
const result = await init({
apiKey: 'es_live_xxxxx',
project: 'my-app',
environment: 'production',
});
console.log(result.variables); // { DATABASE_URL: '...', API_SECRET: '...' }
console.log(result.fromCache); // false (first call is always fresh)
console.log(result.etag); // "abc123" (used for cache revalidation)Returns: { variables: Record<string, string>, fromCache: boolean, etag: string | null }
fetch(): Promise<FetchResult>
Fetch variables without reinitializing. Uses in-memory cache and ETag revalidation to minimize network requests.
const result = await fetch();
// If called within cacheTtl, returns cached data without hitting the networkclear(): void
Clear the in-memory cache, remove all injected variables from process.env, and reset the SDK state. Useful for testing or graceful shutdown.
clear();
// Cache purged, process.env cleaned, SDK resetisInitialized(): boolean
Check whether the SDK has been initialized.
if (!isInitialized()) {
await init({ ... });
}Caching & Resilience
The SDK uses an in-memory cache with ETag revalidation and stale-while-revalidate semantics. This means your app stays resilient even if the EnvShare API is temporarily unavailable.
- Zero dependencies — uses native
fetch(Node 18+) - ETag revalidation — only downloads variables when they've actually changed (HTTP 304)
- Stale fallback — if the API is down, the SDK returns the last known good values
- Automatic retry — retryable errors (network, timeout, rate limit) are retried with exponential backoff
Request Flow:
fetch() called
│
▼
┌──────────┐ YES ┌───────────┐
│ Cache ├───────→│ Return │
│ fresh? │ │ cached │
└────┬─────┘ └───────────┘
│ NO
▼
┌──────────┐ 200 ┌───────────┐
│ Call API ├───────→│ Update │
│ w/ ETag │ │ cache │
└────┬─────┘ └───────────┘
│ 304
▼
┌──────────┐ ┌───────────┐
│ Not ├───────→│ Refresh │
│ Modified │ │ TTL │
└────┬─────┘ └───────────┘
│ ERROR
▼
┌──────────┐ YES ┌───────────┐
│ Stale ├───────→│ Return │
│ cache? │ │ stale │
└────┬─────┘ └───────────┘
│ NO
▼
┌──────────┐
│ THROW │
│ Error │
└──────────┘Cache timeline:
├── cacheTtl (5 min default) ──┤── maxStale (1 hr default) ──┤
│ FRESH │ STALE │ EXPIRED
│ Return instantly │ Try API, fallback to cache │ Must fetchError Handling
All SDK errors extend EnvShareError, so you can catch all SDK errors in one block or handle specific types:
import {
init,
EnvShareError,
AuthError,
ForbiddenError,
NetworkError,
RateLimitError,
NotFoundError,
ConfigError,
TimeoutError,
} from '@alvarorestrepo/envshare-sdk';
try {
await init({
apiKey: process.env.ENVSHARE_API_KEY!,
project: 'my-app',
environment: 'production',
});
} catch (error) {
if (error instanceof AuthError) {
// 401 — API key is invalid, revoked, or expired
console.error('Invalid API key. Generate a new one at envshared.vercel.app');
} else if (error instanceof ForbiddenError) {
// 403 — Your IP is not in the allowlist for this environment
console.error('IP not allowed. Add your IP in Settings → IP Configuration');
} else if (error instanceof NotFoundError) {
// 404 — Project or environment doesn't exist
console.error('Project or environment not found. Check the slug spelling.');
} else if (error instanceof RateLimitError) {
// 429 — Too many requests
console.error(`Rate limited. Retry after ${error.retryAfter}s`);
} else if (error instanceof NetworkError) {
// DNS failure, connection refused, etc.
console.error('Cannot reach EnvShare API');
} else if (error instanceof TimeoutError) {
console.error('Request timed out');
} else if (error instanceof ConfigError) {
console.error('Invalid SDK configuration');
}
}Error Types
| Error | Code | HTTP | Retryable | Cause |
|-------|------|------|-----------|-------|
| ConfigError | CONFIG_ERROR | — | No | Invalid or missing configuration |
| AuthError | AUTH_ERROR | 401 | No | Invalid, revoked, or expired API key |
| ForbiddenError | FORBIDDEN_ERROR | 403 | No | IP not in allowlist for this environment |
| NotFoundError | NOT_FOUND_ERROR | 404 | No | Project or environment not found |
| RateLimitError | RATE_LIMIT_ERROR | 429 | Yes | Too many requests (has retryAfter property) |
| NetworkError | NETWORK_ERROR | — | Yes | Connection/DNS failure |
| TimeoutError | TIMEOUT_ERROR | — | Yes | Request exceeded timeout |
Retryable errors are automatically retried up to maxRetries times with exponential backoff before throwing.
CI/CD Examples
GitHub Actions
steps:
- name: Install dependencies
run: npm ci
- name: Start app with EnvShare
env:
ENVSHARE_API_KEY: ${{ secrets.ENVSHARE_API_KEY }}
ENVSHARE_PROJECT: my-app
ENVSHARE_ENVIRONMENT: staging
run: node --import @alvarorestrepo/envshare-sdk/preload app.jsNote: You must add your GitHub Actions runner IPs to the IP allowlist for the target environment. GitHub publishes their runner IP ranges in their documentation. Alternatively, use a self-hosted runner with a static IP.
Docker
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
ENV ENVSHARE_PROJECT=my-app
ENV ENVSHARE_ENVIRONMENT=production
# Pass ENVSHARE_API_KEY at runtime, not build time:
# docker run -e ENVSHARE_API_KEY=es_live_xxxxx my-app
CMD ["node", "--import", "@alvarorestrepo/envshare-sdk/preload", "app.js"]Note: Never bake
ENVSHARE_API_KEYinto a Docker image. Pass it at runtime via-eor your orchestrator's secrets management (ECS task definition, Kubernetes secret, etc.).
Programmatic in CI
If you prefer programmatic initialization over preload:
// bootstrap.ts — runs before your app
import { init } from '@alvarorestrepo/envshare-sdk';
await init({
apiKey: process.env.ENVSHARE_API_KEY!,
project: process.env.ENVSHARE_PROJECT!,
environment: process.env.ENVSHARE_ENVIRONMENT!,
});
// Now start your app
await import('./app.js');🔒 Security Model
EnvShare is built with defense-in-depth. Multiple security layers protect your secrets:
Encryption at Rest
All environment variables are encrypted with AES-256-GCM before being stored in the database. Each project uses a unique encryption key derived via HKDF (HMAC-based Key Derivation Function) from a master key. Even if the database is compromised, the variables are unreadable without the master key.
API Key Security
API keys are SHA-256 hashed before storage. The raw key (starting with es_live_) is shown exactly once when generated — it is never stored or retrievable again. If a key is compromised, it can be revoked instantly from the platform.
IP Allowlist
Every environment has an independent IP allowlist. A valid API key alone is not enough — the request must also originate from an approved IP address. This prevents stolen API keys from being used outside your infrastructure.
Role-Based Access Control
| Role | Can manage variables | Can manage API keys | Can approve IPs | Can invite members | |------|---------------------|--------------------|-----------------|--------------------| | Owner | Yes | Yes | Yes | Yes | | Admin | Yes | Yes | Yes | Yes | | Developer | Yes | No | No (must request) | No |
Developers can request IP access, but an Owner or Admin must approve it.
Audit Logging
Every API access, key generation, key revocation, IP approval, and variable change is recorded in an audit log with:
- Who performed the action
- When it happened
- From which IP address
- What was affected
Rate Limiting
The API enforces rate limits per IP address and per API key to prevent abuse and brute-force attacks.
All of this is managed through the EnvShare Platform. Sign up to get started.
Comparison: .env Files vs EnvShare SDK
| Feature | .env files | EnvShare SDK |
|---------|-------------|--------------|
| Secrets on disk | Yes (risk) | Never |
| Accidental git commits | Common risk | Impossible |
| Sharing secrets | Slack/email | Encrypted platform |
| Audit trail | None | Full audit log |
| Access control | None | Per-environment IP allowlist |
| Auto-sync across team | Manual | Automatic on restart |
| Encryption at rest | No | AES-256-GCM |
| Revoke access | Change all passwords | Revoke key instantly |
| Per-environment isolation | One .env per environment, manually managed | Built-in, platform-managed |
| Zero dependencies | Requires dotenv | Native fetch (Node 18+) |
Features
- Zero dependencies — uses native
fetch(Node 18+) - In-memory cache with ETag revalidation
- Stale-while-revalidate for resilience when the API is down
- Automatic retry with exponential backoff for transient errors
- TypeScript-first with full type definitions
- ESM + CommonJS dual publish
- Preload mode — zero-code setup with
--import
EnvBridge — Runtime Next.js Support
The Problem: NEXT_PUBLIC_* and Build-Time Replacement
Next.js performs static text replacement on process.env.NEXT_PUBLIC_* references at build time. The reference itself is removed from your code:
// What you write
const url = process.env.NEXT_PUBLIC_API_URL;
// What Next.js outputs after build (the variable reference is GONE)
const url = "https://api.example.com";The EnvShare SDK injects variables into process.env at runtime — but by the time your app runs on the client, those NEXT_PUBLIC_* references have already been replaced with whatever value was available at build time (or undefined if nothing was set).
BUILD TIME (next build) RUNTIME (node server.js)
───────────────────────── ────────────────────────
process.env.NEXT_PUBLIC_API_URL EnvShare SDK injects
→ replaced with literal "..." → process.env.NEXT_PUBLIC_API_URL = "https://..."
→ original reference is GONE → but client code already has the literalResult: Server Components work fine (they read process.env at runtime). Client Components get stale build-time values.
The Solution: EnvBridge + env()
The SDK provides three exports to solve this:
| Export | Type | Purpose |
|--------|------|---------|
| EnvBridge | React Server Component | Reads NEXT_PUBLIC_* from runtime process.env and injects them into window.__ENVSHARE via a <script> tag |
| env(key) | Function | Universal accessor — reads from process.env on the server, from window.__ENVSHARE on the client |
| envRequired(key) | Function | Same as env() but throws a descriptive error if the variable is not defined |
Setup
Step 1: Add EnvBridge to your root layout
// app/layout.tsx
import { EnvBridge } from '@alvarorestrepo/envshare-sdk';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<EnvBridge />
</head>
<body>{children}</body>
</html>
);
}Step 2: Use env() instead of process.env in Client Components
'use client';
import { env } from '@alvarorestrepo/envshare-sdk';
export function ApiStatus() {
const apiUrl = env('NEXT_PUBLIC_API_URL');
return <p>API: {apiUrl}</p>;
}That's it. EnvBridge renders at the server, env() reads the injected values on the client.
Usage Examples
Basic usage in a Client Component:
'use client';
import { env } from '@alvarorestrepo/envshare-sdk';
export function Analytics() {
const trackingId = env('NEXT_PUBLIC_TRACKING_ID');
const apiUrl = env('NEXT_PUBLIC_API_URL');
// Both values come from EnvShare runtime injection,
// not from build-time replacement
return <script src={`https://analytics.example.com/${trackingId}`} />;
}Using envRequired() for mandatory variables:
'use client';
import { envRequired } from '@alvarorestrepo/envshare-sdk';
export function PaymentForm() {
// Throws with a helpful error if NEXT_PUBLIC_STRIPE_KEY is not set:
// "[envshare] Required environment variable "NEXT_PUBLIC_STRIPE_KEY" is not defined.
// Make sure EnvShare SDK is loaded and EnvBridge is in your root layout."
const stripeKey = envRequired('NEXT_PUBLIC_STRIPE_KEY');
return <div data-stripe-key={stripeKey}>...</div>;
}Server Components — just use process.env directly:
// app/dashboard/page.tsx (Server Component — no 'use client')
export default async function DashboardPage() {
// Server Components read process.env at runtime — no EnvBridge needed
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
const secret = process.env.DATABASE_URL; // non-public vars too
const data = await fetch(apiUrl + '/stats');
// ...
}You can also use env() in Server Components — it reads process.env on the server, so it works the same way:
import { env } from '@alvarorestrepo/envshare-sdk';
export default async function DashboardPage() {
const apiUrl = env('NEXT_PUBLIC_API_URL'); // reads process.env on server
// ...
}Custom prefix:
If your app uses a different prefix convention, pass it to EnvBridge:
<EnvBridge prefix="MY_APP_" />This will collect all MY_APP_* variables from process.env and inject them into window.__ENVSHARE. The env() helper reads from window.__ENVSHARE for keys starting with NEXT_PUBLIC_ by default — for custom prefixes, access window.__ENVSHARE directly on the client or use env() on the server.
TypeScript usage:
The env() and envRequired() functions are fully typed:
import { env, envRequired } from '@alvarorestrepo/envshare-sdk';
// env() returns string | undefined
const apiUrl: string | undefined = env('NEXT_PUBLIC_API_URL');
// envRequired() returns string (throws if undefined)
const stripeKey: string = envRequired('NEXT_PUBLIC_STRIPE_KEY');
// Type-safe wrapper for your app's env vars
function getConfig() {
return {
apiUrl: envRequired('NEXT_PUBLIC_API_URL'),
analyticsId: env('NEXT_PUBLIC_ANALYTICS_ID') ?? 'default',
debug: env('NEXT_PUBLIC_DEBUG') === 'true',
} as const;
}How It Works
1. Server startup
┌────────────────────────────────┐
│ EnvShare SDK init() │
│ → fetches vars from API │
│ → injects into process.env │
└──────────────┬─────────────────┘
│
2. SSR render (every request)
┌──────────────┴─────────────────┐
│ <EnvBridge /> (Server Component)│
│ → reads NEXT_PUBLIC_* from │
│ process.env at runtime │
│ → renders <script> tag with │
│ JSON-serialized values │
└──────────────┬─────────────────┘
│
3. Client hydration
┌──────────────┴─────────────────┐
│ Browser executes <script> │
│ → window.__ENVSHARE = {...} │
└──────────────┬─────────────────┘
│
4. Client Components
┌──────────────┴─────────────────┐
│ env('NEXT_PUBLIC_API_URL') │
│ → reads window.__ENVSHARE │
│ → returns runtime value │
└────────────────────────────────┘Comparison
| Approach | Server Components | Client Components | No extra HTTP | Immediate |
|----------|:-:|:-:|:-:|:-:|
| process.env (build-time) | ✅ | ❌ runtime vars | ✅ | ✅ |
| EnvBridge + env() | ✅ | ✅ | ✅ | ✅ |
| API route | ✅ | ✅ | ❌ | ❌ |
Important Notes
- Replace
process.env.NEXT_PUBLIC_*withenv('NEXT_PUBLIC_*')in Client Components. This is the only change needed —env()handles the server/client logic automatically. - Server Components can still use
process.envdirectly. They run at request time and read the runtime values without any bridge. EnvBridgemust be in the root layout (app/layout.tsx). If it's only in a nested layout, pages outside that layout won't have access to the injected values.- React 18+ is required for
EnvBridge(it's a React Server Component). React is an optional peer dependency of the SDK. - Don't put secrets in
NEXT_PUBLIC_*variables.EnvBridgeserializes values as JSON in the HTML — they are visible in the page source. This is the same behavior as Next.js build-time replacement. Only useNEXT_PUBLIC_*for values that are safe to expose to the browser.
Requirements
- Node.js >= 18.0.0
- An EnvShare account with:
- At least one project and environment
- An API key
- Your IP(s) added to the allowlist
Links
- Platform: envshared.vercel.app
- GitHub: github.com/GooDevPro/envShare
- npm: @alvarorestrepo/envshare-sdk
License
MIT
