
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 dotenv
Learn 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 start
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.
# 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 →
# 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
Migrating 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.