Declarative secrets, every environment, any provider
Why Secrets Are Still Hard
Section titled “Why Secrets Are Still Hard”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.
WHAT - Declaring Your Secrets
Section titled “WHAT - Declaring Your Secrets”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 }# Initialize secretspec.toml, possibly from `.env`$ secretspec init --from dotenvLearn more about declarative configuration →
HOW - Managing Requirements with Profiles
Section titled “HOW - Managing Requirements with Profiles”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 changesDATABASE_URL = { default = "postgresql://localhost/myapp_dev" }REDIS_URL = { default = "redis://localhost:6379" }# 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 startWHERE - Flexible provisioning with Providers
Section titled “WHERE - Flexible provisioning with Providers”The same application works across different secret storage backends without any code changes.
# 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.tomlSupported 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 →
# 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 startMigrating Between Providers
Section titled “Migrating Between Providers”SecretSpec makes it easy to migrate your secrets between different providers without changing your application code.
# 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.
Type-Safe Rust SDK
Section titled “Type-Safe Rust SDK”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.tomlsecretspec_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.