Storing Secrets and API keys
Keep credentials out of your code
API keys, database passwords, and private keys don't belong in source code. Encore.rs lets you declare a secret in code, store its value with Encore's secrets manager, and read it at runtime.
Declaring a secret
Declare a secret as a module-level static, then read it with value():
billing/src/stripe.rsuse encore::secrets::Secret;
static STRIPE_KEY: Secret = Secret::new("StripeKey");
fn authorization() -> Result<String, encore::Error> {
Ok(format!("Bearer {}", *STRIPE_KEY.value()?))
}
Encore reads secret declarations from your source code, so a few rules apply:
- A secret must be declared as a module-level
static, not inside a function. - The name must be a string literal. It's an identifier: a letter or underscore, followed by letters, digits, and underscores.
newisconst, so declaring a secret does nothing at runtime. The value is looked up when you callvalue().
Scopes
The type you declare a secret with decides who it belongs to:
use encore::secrets::{GlobalSecret, Secret, SharedSecret};
// This service's own signing key.
static SIGNING_KEY: Secret = Secret::new("SigningKey");
// Shared with other services that read it.
static GREETING: SharedSecret = SharedSecret::new("Greeting");
// Available everywhere.
static LICENSE: GlobalSecret = GlobalSecret::new("License");
Because a Secret is service-scoped, adding one to a service can never collide with, or silently reuse, a name another service already uses.
Reading values
value() returns Result<Value<T>, encore::Error>. A Value<T> is a cheap, cloneable handle that dereferences to the secret's value:
let key = STRIPE_KEY.value()?;
let header = format!("Bearer {}", *key);
assert!(key.starts_with("sk_")); // String methods work directly
A secret's value can change while the app runs. A handle keeps the value it was made with, and the next call to value() returns the new one. Call value() where you use the secret rather than storing a handle for the lifetime of the process.
Value<T>'s Debug implementation prints Value(<secret>), so a handle that ends up in a log line or a panic message doesn't leak the secret.
Errors
value() returns an error when:
- The secret has no value in the current environment (
ErrCode::NotFound). - The stored bytes can't be read as the secret's type, such as invalid UTF-8 for a
Stringor malformed JSON for aJson<T>(ErrCode::Internal). The error message says where the JSON is wrong, never what it contains.
Value types
A secret is a String unless you give it another type:
use encore::secrets::{GlobalSecret, Json, Secret};
use serde::Deserialize;
// Raw bytes.
static SIGNING_KEY: Secret<Vec<u8>> = Secret::new("SigningKey");
// A JSON document, deserialized with serde.
#[derive(Deserialize)]
struct Limits {
max_users: u32,
}
static LIMITS: GlobalSecret<Json<Limits>> = GlobalSecret::new("Limits");
fn max_users() -> Result<u32, encore::Error> {
// Json<T> dereferences to the T inside.
Ok(LIMITS.value()?.max_users)
}
The built-in types are String, Vec<u8>, and Json<T> for any T: serde::de::DeserializeOwned. To read a secret as a type of your own, implement the SecretValue trait's from_bytes(bytes: &[u8]) -> Result<Self, encore::Error>.
Setting secret values
Use the encore secret command to set values. The value is read from stdin, so you can pipe it in, or you're prompted for it without echo when running interactively.
On your machine
By default, encore secret set stores the value on your machine only, in .secrets.local.toml in the app root:
$ encore secret set StripeKey
A bare name refers to the app-wide secret (a SharedSecret or GlobalSecret). A service's own Secret is named with the service, either as service.Name or with --service:
$ encore secret set billing.SigningKey$ encore secret set --service billing SigningKey
The name is checked against the secrets your app declares. Run encore secret set without a name to be prompted for each declared secret that has no local value yet.
The file is a plain TOML file, and you can also edit it by hand. App-wide secrets go in [shared], and each service's own in [services.<name>]:
.secrets.local.toml[shared]
StripeKey = "sk_test_123"
[services.billing]
SigningKey = "local-signing-key"
Local values are never uploaded, and they take precedence over values from Encore Cloud. Keep the file out of version control; encore secret set adds it to .gitignore for you.
For your team and deployed environments
To store a value in Encore Cloud, name the environment types it applies to with --type, or specific environments with --env:
$ encore secret set --type production StripeKey$ encore secret set --type development,preview,local StripeKey$ encore secret set --env staging StripeKey
Environment types are production, development, preview, and local (shorthands: prod, dev, pr). A value set for a named environment takes precedence over the value for its type. Values for local are synced to every teammate's encore run and encore test.
Encore Cloud doesn't store per-service secret values yet. A service's own Secret can only be given a value on your machine; use a SharedSecret or GlobalSecret for values that deployed environments need.
Listing and removing values
encore secret list shows which secrets have values and where, without printing the values. encore secret list --missing lists the declared secrets that have no value and exits non-zero if there are any, which is useful in CI.
encore secret unset removes a value. It takes the same flags as set: without a flag it removes your local value, and --type or --env removes a value from Encore Cloud.
Secrets in config
A field of a config type can read a secret instead of holding a value:
#[derive(serde::Deserialize, encore::config::ConfigType)]
struct Stripe {
endpoint: String,
#[config(secret = "StripeKey")]
api_key: encore::secrets::Secret,
}
The secret takes the config's scope and must also be declared in code. See Configuration for details.
Testing
Under encore test, a test can give a secret a value of its own with encore::testing::set_secret. No other test sees it, and it's gone when the test ends:
#[encore::test]
async fn uses_the_test_key() -> Result<(), encore::Error> {
encore::testing::set_secret(&STRIPE_KEY, "sk_test_fake")?;
assert_eq!(*STRIPE_KEY.value()?, "sk_test_fake");
// Any secret type works: the value is the raw bytes.
encore::testing::set_secret(&SIGNING_KEY, [1, 2, 3])?;
encore::testing::set_secret(&LIMITS, r#"{"max_users": 2}"#)?;
Ok(())
}
The value is given as bytes (anything that implements AsRef<[u8]>) and read as the secret's type when you call value(), so a value that doesn't parse makes value() return an error. See Testing for more.
How secrets are stored
Values you set in Encore Cloud are stored encrypted. When you deploy to your own cloud account on AWS or GCP, Encore replicates them to a secrets manager in that account and provides them to your services at runtime. For local development, encore run fetches the local values from Encore Cloud and layers your .secrets.local.toml on top.