Documentation

Using ValtHub

ValtHub stores your team's environment variables and secrets, encrypted at rest, organized by project and environment, with role-based access. This guide covers everything the app does today.

Introduction

ValtHub is a web application for managing API keys, tokens, and environment variables across multiple environments (development, staging, production). Secrets are encrypted with AES-256 and never shown in plaintext until you explicitly reveal them. Access is scoped to a workspace and controlled by roles.

You manage secrets through the web UI. For programmatic access at runtime — from your app, backend, or CI — use scoped remote config keys with the REST endpoint or an official SDK.

Quickstart

  1. Sign in with GitHub, Google, or email at /register.
  2. Create a workspace — on first sign-in you land on a welcome page. A workspace is your team; it gets a URL-friendly slug you can edit later in workspace settings.
  3. Create a project — one per application, each with its own slug, to keep secrets isolated.
  4. Add environments — e.g. development, staging, production.
  5. Add secrets — a key (e.g. DATABASE_URL) and value per environment. Values are encrypted and stay masked until you reveal them.
  6. Invite your team — to the whole workspace or a single project, as editor or viewer.

Core concepts

Workspace
The top-level container (your team). Holds projects and members. Each user can belong to multiple workspaces.
Project
A group of related secrets, usually one per application or service. Has a name and a URL-safe slug.
Environment
A stage within a project (development, staging, production, …). Each environment holds its own set of variables.
Secret / Variable
A key–value pair inside an environment. Marked secret (masked) or plain, with a data type (string, integer, boolean, json, url, …).

Managing secrets

Inside a project you get a table of keys per environment. You can:

  • Reveal per row — values are masked by default; click the eye to reveal one row (independent per row), or Reveal all.
  • Copy — copy a value to the clipboard even while masked.
  • Search — filter by key name (case-insensitive).
  • Groups — keys auto-group by prefix before the first underscore (AZURE_*, KORONA_*), collapsible.
  • Edit — reveal a row to edit its value, then Save.
  • Delete — type-to-confirm the exact key; delete from the current environment or all.

Environments

Environments carry a risk level, shown consistently across the app with a colored dot:

  • Development — safe
  • Staging — caution
  • Production — protected (shown with a lock)

Each environment tab shows its key count. Add a new environment from the New environment button.

Compare environments

From a project's Secrets tab, click Compare and pick a second environment. The table switches to a side-by-side diff of the two — a source and a target you can swap. Values stay masked (reveal per row, or reveal all). Rows are highlighted:

  • Different — the key exists in both but the values differ.
  • Missing — present in only one side; the empty column shows — missing.
  • Rows in sync are hidden under the differences-only filter.

Sync copies selected keys from the source into the target. If the target is a protected environment (see below), you must type its name to confirm before anything is written, and the diff must be current — a stale comparison is rejected so you never overwrite a value that changed since you looked.

Import & export

Import from a .env or JSON file into an environment. Export an environment as:

  • .env — download or copy to clipboard
  • JSON — download or copy to clipboard

Exports include real values plus a metadata watermark (environment, project, workspace, timestamp, source).

Inviting your team

You can invite people at two levels:

  • Workspace invitation — from Team. The person joins the whole workspace and can see its projects.
  • Project invitation — from a project's Members tab. The person gets access to that project only — nothing else in the workspace.

Project members hold one of two roles:

RoleCan do
EditorRead and reveal values, add / edit / delete secrets, import, and sync between environments.
ViewerRead and reveal values and export — read-only. No edits, deletes, or syncs.

Workspace owners and admins act as admins on every project in the workspace. Roles are changed or members removed from the member's actions menu.

Email-match rule. An invitation can only be accepted by the exact email address it was sent to. If you're signed in with a different address, ValtHub asks you to sign in with the invited one — knowing the invite link alone is never enough.

Security

  • AES-256 encryption at rest — secret values are encrypted; never stored in plaintext.
  • Masked by default — values are hidden in the UI (and in Compare) until explicitly revealed.
  • JWT sessions — authentication uses signed tokens; passwords are hashed (PBKDF2).
  • Email verification and password reset are built in.
  • Audit trail — platform activity is logged for administrators.

Plans & limits

The Free plan includes:

  • 1 workspace
  • Up to 3 projects
  • Up to 5 environments per project
  • Up to 100 secrets per project
  • Up to 10 team members

Higher limits (Pro) are planned.

Remote config API

This is the only API you need for runtime access. Generate scoped, read-only keys under Integrations, then fetch a project's config from your app, backend, or CI — over the REST endpoint or an official SDK. There is no login/JWT flow to wire up: the key is the credential.

Keys & scope

  • Each key is scoped to one project and to specific flavors (environments).
  • sh_live_ server keys read all values, including secrets.
  • sh_pub_ client keys receive non-secret values only and can never be scoped to production.
  • Revoke, rename, or re-scope any key at any time. Every fetch is written to the audit log.

The endpoint

One request returns the whole environment as a flat key–value map. Pass project and flavor (the environment slug).

curl "$API/api/v1/config?project=my-app&flavor=production" \
  -H "Authorization: Bearer sh_live_..."
# -> {
#      "project": "my-app",
#      "flavor": "production",
#      "version": "v42",
#      "generatedAt": "2026-07-11T00:00:00.000Z",
#      "values": { "DATABASE_URL": "...", "API_KEY": "..." }
#    }

Send If-None-Match: "v42" to get a cheap304 Not Modified when the config hasn't changed — cache theversion and revalidate cheaply on each boot.

Node / JavaScript

Plain fetch — no SDK to install.

const res = await fetch(
  `${process.env.VALTHUB_URL}/api/v1/config?project=my-app&flavor=production`,
  { headers: { Authorization: `Bearer ${process.env.VALTHUB_KEY}` } }
);
const { values } = await res.json();
// values.DATABASE_URL, values.API_KEY, ...

Encrypted payloads

A key can be created with Encrypt response payload switched on. ValtHub then mints a cipher secret (shc_…), shown once next to the key. Responses for that key carry an AES-256-GCM envelope instead of a readable values object.

# -> {
#      "project": "my-app", "flavor": "production", "version": "v42",
#      "encrypted": true,
#      "enc": { "alg": "AES-256-GCM", "v": 1, "iv": "<base64>", "data": "<base64>" }
#    }

import { webcrypto as crypto } from "node:crypto";

const raw = Buffer.from(process.env.VALTHUB_CIPHER.replace(/^shc_/, ""), "base64url");
const key = await crypto.subtle.importKey("raw", raw, "AES-GCM", false, ["decrypt"]);
// The AAD binds the payload to this project/flavor/version — rebuild it exactly.
const aad = new TextEncoder().encode(
  `valthub-config|v${enc.v}|${project}|${flavor}|${version}`
);
const plain = await crypto.subtle.decrypt(
  { name: "AES-GCM", iv: Buffer.from(enc.iv, "base64"), additionalData: aad },
  key,
  Buffer.from(enc.data, "base64")
);
const values = JSON.parse(new TextDecoder().decode(plain));

The iv is fresh per response and the tag is appended to data, so tampering or replaying another flavor's payload fails to decrypt rather than returning the wrong config. ETag/304 caching works exactly as before. Rotate the secret from a key's actions menu — the previous one stops working immediately, so deploy first.

What this does and doesn't buy you. An app that ships the API key also ships the cipher secret, so this is not protection against someone reverse-engineering your binary — for that, use client keys and keep secrets server-side. What it does give you is a payload that stays opaque to everything it merely passes through: TLS-terminating proxies, CDN and browser caches, request logs, and the SDK's on-disk cache.

In CI (GitHub Actions)

Store the key as a CI secret and pull config at the start of a job.

- name: Load config from ValtHub
  run: |
    curl -sf "$VALTHUB_URL/api/v1/config?project=my-app&flavor=production" \
      -H "Authorization: Bearer $VALTHUB_KEY" > config.json
  env:
    VALTHUB_URL: ${{ vars.VALTHUB_URL }}
    VALTHUB_KEY: ${{ secrets.VALTHUB_KEY }}

Generate keys, set their scope, and try live requests from Integrations in the dashboard.

FAQ

What happens if I accept an invitation with the wrong email?

You can't. An invitation only accepts the exact address it was sent to. Sign in with the invited email, or ask the inviter to re-send it to the address you use.

How does the project quota work?

The Free tier allows up to 3 projects that you own. Projects you only collaborate on (via a workspace or project invitation) don't count against your limit. New accounts can create 1 project until their email is verified.

How are values encrypted?

Secret values are encrypted with AES-256-GCM before they're stored, and are never kept in plaintext. In the UI they stay masked until you explicitly reveal a row (which is recorded in the audit log). This is server-side encryption with access controls — not a zero-knowledge scheme, and we don't claim otherwise.

Can I copy values between environments?

Yes — use Compare & sync. Copying into a protected environment requires typing its name to confirm, and a stale diff is rejected.