Skip to content

Declarative secrets, every environment, any provider

Current secret management forces applications to answer three questions at once:

  • WHAT - Which secrets does the application need? (DATABASE_URL, API_KEY)
  • HOW - What are the requirements? (required vs optional, defaults, validation, environment)
  • WHERE - Where are these secrets stored? (environment variables, Vault, AWS Secrets Manager)

This coupling creates vendor lock-in, runtime failures, poor developer experience, and inconsistent practices.

SecretSpec solves this by separating secret declaration from secret provisioning. Your application declares what secrets it needs in secretspec.toml, but the actual secret values are retrieved at runtime from your chosen provider - never stored in configuration files.

Applications declare their secret requirements in a secretspec.toml file.

Each secret is defined with its name and description, creating a single source of truth that’s version controlled alongside your code. This standardized format enables ecosystem-wide tooling and ensures every developer knows exactly what secrets the application needs.

Important: The secretspec.toml file only declares which secrets your application needs and their requirements - it never contains actual secret values. Secret values are always retrieved at runtime from your configured provider.

[project]
name = "my-app"
revision = "1.0"
[profiles.default]
DATABASE_URL = { description = "PostgreSQL connection string", required = true }
REDIS_URL = { description = "Redis connection string", required = false }
Terminal window
# Initialize secretspec.toml, possibly from `.env`
$ secretspec init --from dotenv

Learn more about declarative configuration →

SecretSpec’s profile system allows you to specify different requirements, defaults, and validation rules for development, staging, production, or any custom environment.

A secret might be optional with a local default in development but required in production - all without changing your application code.

[project]
name = "my-app"
revision = "1.0"
[profiles.default]
DATABASE_URL = { description = "PostgreSQL connection string", required = true }
REDIS_URL = { description = "Redis connection string", required = false }
[profiles.development]
# Inherits from default profile - only override what changes
DATABASE_URL = { default = "postgresql://localhost/myapp_dev" }
REDIS_URL = { default = "redis://localhost:6379" }
Terminal window
# Run with a specific profile
$ secretspec run --profile development -- npm start
$ secretspec run --profile production -- npm start
# Or use environment variables
$ SECRETSPEC_PROFILE=development secretspec run -- npm start
$ SECRETSPEC_PROFILE=production secretspec run -- npm start

Learn more about profiles →

WHERE - Flexible provisioning with Providers

Section titled “WHERE - Flexible provisioning with Providers”

The same application works across different secret storage backends without any code changes.

Terminal window
# Configure your default provider interactively
$ secretspec config init
? Select your preferred provider backend:
> keyring: Uses system keychain (Recommended)
onepassword: OnePassword password manager
dotenv: Traditional .env files
env: Read-only environment variables
lastpass: LastPass password manager
? Select your default profile:
> development
default
none
Configuration saved to ~/.config/secretspec/config.toml

Supported providers:

  • Keyring - System credential store (Keychain on macOS, Credential Manager on Windows, Secret Service on Linux)
  • Dotenv - Traditional .env files for local development
  • Environment - Read-only access to environment variables for CI/CD
  • OnePassword - Team-based password management
  • LastPass - Cloud-based password manager

Learn how to add a new provider →

Terminal window
# Check all secrets are available and set them if not
$ secretspec check
$ secretspec set DATABASE_URL
# Override provider for specific commands
$ secretspec run --provider env -- npm test
$ secretspec run --provider onepassword://vault -- npm start
# Or use environment variables
$ SECRETSPEC_PROVIDER=env secretspec run -- npm test
$ SECRETSPEC_PROVIDER=onepassword://vault secretspec run -- npm start

Learn more about providers →

SecretSpec makes it easy to migrate your secrets between different providers without changing your application code.

Terminal window
# Import all secrets from one provider to another
$ secretspec import dotenv://.env.production
Imported 5 secrets from dotenv://.env.production to keyring://

This separation enables portable applications with lower operational overhead when switching providers.

While the CLI is great for development workflows, integrating SecretSpec directly into your application provides better type safety and error handling.

The Rust SDK generates strongly-typed structs from your secretspec.toml, ensuring compile-time verification of your secret access.

// Generate typed structs from secretspec.toml
secretspec_derive::declare_secrets!("secretspec.toml");
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load secrets using the builder pattern
let secrets = Secrets::builder()
.with_provider("keyring") // Can use provider name or URI like "dotenv:/path/to/.env"
.with_profile("production") // Can use string or Profile enum
.load()?; // All conversions and errors are handled here
// Access secrets (field names are lowercased)
println!("Database: {}", secrets.secrets.database_url); // DATABASE_URL → database_url
// Optional secrets are Option<String>
if let Some(redis_url) = &secrets.secrets.redis_url {
println!("Redis: {}", redis_url);
}
// Set all secrets as environment variables
secrets.secrets.set_as_env_vars();
Ok(())
}

Learn more about the Rust SDK →

SDKs for other languages are welcome! Please see our contribution guide if you’d like to help.


SecretSpec was designed by Cachix for devenv.sh. See the announcement post.