Reference / Env Vault

Env Vault

Inject env into your deploy with a single bootstrap token; later changes don't need a rebuild. Use getEnv() on the server or useEnv() on the React side — public/private split is automatic.

Overview#

Env Vault provides runtime env management for self-hosted applications. Instead of using process.env, your app authenticates to Sentroy core with a single API key and pulls every registered env. When an admin changes a value, your app picks it up after the cache TTL expires — no Docker rebuild required.

Flow

1. Create a project in the Sentroy admin panel (e.g. my-blog).
2. Add environments (e.g. dev, prod) and variables under the project. Each variable carries a public flag.
3. Generate a token for the project + environment pair. The plaintext is shown only once.
4. Set the token as SENTROY_ENV_API_KEY in your deploy environment.
5. Use getEnv("KEY") or useEnv("KEY") in your app.

Bootstrap#

Single env: SENTROY_ENV_API_KEY. Everything else comes from the vault.

# Coolify (or any deploy environment)
SENTROY_ENV_API_KEY=stk_...
# Optional — defaults to https://sentroy.com
SENTROY_ENV_API_URL=https://sentroy.com

That's it. Every other env (DATABASE_URL, BETTER_AUTH_SECRET, NEXT_PUBLIC_TURNSTILE_SITE_KEY, etc.) lives in the Sentroy admin panel.

Install#

The vault lives under the /vault subpath of @sentroy-co/client-sdk — same package as the mail/storage SDK.

npm install @sentroy-co/client-sdk

Server-side: getEnv()#

Async, in-memory cache (TTL 5 min). One HTTP fetch on first call — every subsequent call is in-process.

import { getEnv, getEnvOrThrow, preloadEnv } from "@sentroy-co/client-sdk/vault"

// Load early at process boot — fail-fast on missing envs
await preloadEnv()

// Async — returns undefined if the env is missing
const dbUrl = await getEnv("DATABASE_URL")

// Throws if missing — config-validation pattern
const turnstile = await getEnvOrThrow("BETTER_AUTH_TURNSTILE_SECRET")

Cache

Default TTL is 5 minutes. For webhook- or admin-driven invalidation use refreshEnvCache(). To change the TTL at runtime use setEnvCacheTTL(seconds).

React: useEnv()#

Inject public envs from a server component during SSR; the client useEnv() hook reads them synchronously.

// app/layout.tsx (server component)
import { getPublicEnvs } from "@sentroy-co/client-sdk/vault"
import { EnvProvider } from "@sentroy-co/client-sdk/vault/react"

export default async function RootLayout({ children }) {
  const envs = await getPublicEnvs()
  return (
    <html>
      <body>
        <EnvProvider envs={envs}>{children}</EnvProvider>
      </body>
    </html>
  )
}
// any client component
"use client"
import { useEnv } from "@sentroy-co/client-sdk/vault/react"

export function CaptchaWidget() {
  const siteKey = useEnv("TURNSTILE_SITE_KEY")
  if (!siteKey) return null
  return <Turnstile siteKey={siteKey} />
}

CLI#

Sync a local .env file to the vault from your terminal or CI.

The SDK ships a sentroybinary. After install it's available on $PATH; npx sentroy ... works without a global install. Auth uses the same SENTROY_ENV_API_KEY as getEnv() (or pass --token=stk_env_...). The token's (project, environment) scope is implicit.

# Push local file to the vault. --delete-missing makes it a full sync;
# without it, push is upsert-only. The CLI prompts before deletes.
sentroy env push .env.production --delete-missing

# Show the diff but write nothing.
sentroy env push .env.production --dry-run

# Pull the vault into a local file. --force overwrites.
sentroy env pull .env.staging --force

# List keys (add --values for KEY=value, --public-only to filter).
sentroy env list --values

REST API#

Direct access via curl/fetch, without the SDK.

GET /api/env-vault/fetch

Returns every env in the token's scope (public + private). For server-side use.

curl -H "Authorization: Bearer stk_..." \
  https://sentroy.com/api/env-vault/fetch

GET /api/env-vault/public

Only variables flagged public: true. Browser-safe.

curl -H "Authorization: Bearer stk_..." \
  https://sentroy.com/api/env-vault/public

Webhooks#

Real-time invalidation — skip the 5 min cache TTL when a value changes.

Configure a webhook on a project + environment in the vault dashboard. Whenever any variable changes (create, update, or delete), Sentroy POSTs to your URL with an HMAC-SHA256 signature. The default SDK handler verifies the signature and calls refreshEnvCache() — the next getEnv() hits a fresh fetch.

// app/api/sentroy/vault-webhook/route.ts
import { createVaultWebhookHandler } from "@sentroy-co/client-sdk/vault"

export const POST = createVaultWebhookHandler({
  secret: process.env.SENTROY_VAULT_WEBHOOK_SECRET!,
})

The receiver URL goes into the dashboard; the secret comes back once at create-time and is set as SENTROY_VAULT_WEBHOOK_SECRET in the consuming app.

Payload

{
  "event": "vault.variable.changed",
  "project": "<projectId>",
  "environment": "prod",
  "action": "create" | "update" | "delete",
  "keys": ["DATABASE_URL", "..."],
  "timestamp": 1731430000000
}

Headers: X-Sentroy-Signature: sha256=<hex> (HMAC over the raw body), X-Sentroy-Event: vault.variable.changed, X-Sentroy-Webhook-Id. Delivery is fire-and-forget with a 5 sec timeout — last status + error are recorded in the dashboard for visibility but failed deliveries are not retried automatically.

Custom handler

Override the default cache-clear with onChange — useful for targeted invalidation, structured logging, or downstream notifications.

export const POST = createVaultWebhookHandler({
  secret: process.env.SENTROY_VAULT_WEBHOOK_SECRET!,
  async onChange(payload) {
    console.log("vault changed", payload.action, payload.keys)
    await refreshEnvCache()
  },
})

Encryption#

Variable values are encrypted at rest with AES-256-GCM.

The master key lives on Sentroy core in the SENTROY_ENV_MASTER_KEY env. Plaintext is never written to the database — only ciphertext + nonce + auth tag (v1:iv:tag:cipher base64). Decryption happens in the token-auth fetch response.

# Generate a master key (one-time, store in Coolify env)
openssl rand -base64 32
# Add to platform: SENTROY_ENV_MASTER_KEY=<output>

Audit log#

Every change records who/what/when — never the value itself.

The audit log never stores the value itself; it writes a sha256(plaintext)checksum as before/after. That makes "did the value change?" comparable, while a log compromise will not leak any plaintext.