smooai-config 6.7.0

Type-safe three-tier configuration management (public, secret, feature flags) with schema validation and a runtime client for the Smoo AI config platform.
Documentation

About SmooAI

SmooAI is an AI-powered platform for helping businesses multiply their customer, employee, and developer experience.

Learn more on smoo.ai

SmooAI Packages

Check out other SmooAI packages at smoo.ai/open-source

About smooai-config (Rust)

Type-safe config, secrets, and feature flags for Rust - Same schema, same keys, same source of truth as your TypeScript, Python, Go, and .NET services. All strongly typed, all async.

Crates.io Version Crates.io Downloads Crates.io License

GitHub License GitHub Actions Workflow Status GitHub Repo stars

Rust Crate

Rust port of @smooai/config. Derive JsonSchema on your own Rust structs, generate the exact schema every other service in your stack reads, and resolve values through a cached async client.

Note: the smooai-config CLI (push / pull / list / set / diff / login) is TypeScript-only. The schema is authored in TS and pushed via the CLI; this Rust crate only reads values at runtime. If you're on a Rust-only team and need the CLI, install it via Node: pnpm add -g @smooai/config (or npm i -g @smooai/config).

What you get

  • Three tiers, one schema - public config, secrets, and feature flags as three Rust structs with #[derive(JsonSchema)].
  • Strongly-typed, idiomatic Rust - define_config_typed::<Public, Secret, Flags>() turns your structs into the schema every other service reads.
  • Any environment, any key - same API for development, staging, production with per-stage overrides.
  • Cross-language source of truth - the same schema lives in TypeScript, Python, Go, and .NET services.
  • Zero-config client setup - ConfigClient::from_env() picks up SMOOAI_CONFIG_* and goes.
  • Async + cached - fetched values stay in-process between calls; invalidate on demand or set a TTL.

Install

Add to your Cargo.toml:

[dependencies]
smooai-config = "0.1"

or using cargo:

cargo add smooai-config

All Language Packages

Language Package Install
TypeScript @smooai/config pnpm add @smooai/config
Python smooai-config pip install smooai-config
Rust smooai-config cargo add smooai-config
Go github.com/SmooAI/config/go/config go get github.com/SmooAI/config/go/config
.NET SmooAI.Config dotnet add package SmooAI.Config

Usage

Define Configuration Schemas with Native Rust Types

The preferred way to define configuration is with Rust structs that derive JsonSchema. Use EmptySchema for tiers that have no configuration values:

use smooai_config::schema::{define_config_typed, EmptySchema};
use schemars::JsonSchema;
use serde::{Serialize, Deserialize};

#[derive(Default, Serialize, Deserialize, JsonSchema)]
struct PublicConfig {
    api_url: String,
    max_retries: u32,
    enable_debug: bool,
}

#[derive(Default, Serialize, Deserialize, JsonSchema)]
struct SecretConfig {
    database_url: String,
    api_key: String,
}

#[derive(Default, Serialize, Deserialize, JsonSchema)]
struct FeatureFlags {
    enable_new_ui: bool,
    beta_features: bool,
}

// Generates JSON Schema from your Rust types and validates cross-language compatibility
let config = define_config_typed::<PublicConfig, SecretConfig, FeatureFlags>();

println!("{}", serde_json::to_string_pretty(&config.json_schema).unwrap());

Define Configuration Schemas from Raw JSON Schema

Alternatively, pass raw JSON Schema values directly:

use smooai_config::schema::define_config;

let public_schema = serde_json::json!({
    "type": "object",
    "properties": {
        "api_url": {"type": "string"},
        "max_retries": {"type": "integer"}
    }
});

let config = define_config(
    Some(public_schema),
    None, // no secret tier
    None, // no feature flags
);

Runtime Client - Fetch Values from Server

The ConfigClient is async and uses reqwest under the hood. Before each call it mints a short-lived JWT via the OAuth2 client_credentials grant against {auth_url}/token (cached and auto-refreshed via [TokenProvider]). Fetched values are cached locally until invalidate_cache is called or a TTL expires. Note: SDK versions prior to SMOODEV-975 sent the raw API key as Bearer — the backend rejects that flow with 401.

use smooai_config::ConfigClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Option 1: Use environment variables (zero-config)
    // Reads SMOOAI_CONFIG_API_URL, SMOOAI_CONFIG_CLIENT_ID,
    // SMOOAI_CONFIG_CLIENT_SECRET (or legacy SMOOAI_CONFIG_API_KEY),
    // SMOOAI_CONFIG_ORG_ID, SMOOAI_CONFIG_AUTH_URL (defaults to https://auth.smoo.ai).
    let mut client = ConfigClient::from_env();

    // Option 2: Explicit configuration
    let mut client = ConfigClient::new(
        "https://config.smooai.dev",
        "your-client-id",
        "your-client-secret",
        "your-org-id",
    );

    // Option 3: Explicit with default environment
    let mut client = ConfigClient::with_environment(
        "https://config.smooai.dev",
        "your-client-id",
        "your-client-secret",
        "your-org-id",
        "production",
    );

    // Fetch a single value (None uses default environment)
    let api_url = client.get_value("API_URL", None).await?;

    // Fetch with environment override
    let staging_url = client.get_value("API_URL", Some("staging")).await?;

    // Fetch all values
    let all_values = client.get_all_values(None).await?;

    Ok(())
}

Caching

Cache TTL can be configured with set_cache_ttl. By default the cache never expires (manual invalidation only):

use smooai_config::ConfigClient;
use std::time::Duration;

let mut client = ConfigClient::new(
    "https://config.smooai.dev",
    "your-api-key",
    "your-org-id",
);

// Set a 5-minute TTL
client.set_cache_ttl(Some(Duration::from_secs(300)));

// Fetched from server and cached
let value = client.get_value("API_URL", None).await?;

// Served from cache
let value = client.get_value("API_URL", None).await?;

// Invalidate all cached values
client.invalidate_cache();

// Invalidate cached values for one environment
client.invalidate_cache_for_environment("production");

Local Configuration Manager

For local development or offline environments, LocalConfigManager loads configuration from .smooai-config/ files and environment variables:

use smooai_config::LocalConfigManager;

let manager = LocalConfigManager::new(None, None, None)?;

// Fetch values from local file config + env vars
let api_url = manager.get_public_config("API_URL")?;
let db_url = manager.get_secret_config("DATABASE_URL")?;
let new_ui = manager.get_feature_flag("ENABLE_NEW_UI")?;

Baked Runtime — zero-network cold starts

For Lambda / ECS / long-lived services, bake every public + secret value into an AES-256-GCM blob at deploy time and decrypt it at cold start. build_config_runtime decrypts the blob and seeds the manager's merged config map, so public/secret reads resolve from in-memory cache with no HTTP round-trip. Feature flags are skipped (the baker drops them) so they stay live-fetched.

use smooai_config::{build_config_runtime, RuntimeOptions};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Reads SMOO_CONFIG_KEY_FILE + SMOO_CONFIG_KEY, decrypts the blob, and
    // seeds the manager. With no env vars set, returns a regular live-fetch
    // ConfigManager — same API either way.
    let manager = build_config_runtime(RuntimeOptions {
        environment: Some("production".to_string()),
        ..Default::default()
    })
    .await?;

    let api_url = manager.get_public_config("apiUrl")?;
    let sendgrid = manager.get_secret_config("sendgridApiKey")?;
    Ok(())
}

Bake the bundle at deploy time:

use std::collections::HashSet;
use smooai_config::{build_bundle, BuildBundleOptions, Classification};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Schema-driven classifier — feature flags must return Skip so they stay live.
    let public_keys: HashSet<String> = ["apiUrl"].into_iter().map(String::from).collect();
    let secret_keys: HashSet<String> = ["sendgridApiKey"].into_iter().map(String::from).collect();
    let flag_keys: HashSet<String> = ["newFlow"].into_iter().map(String::from).collect();

    let result = build_bundle(BuildBundleOptions {
        base_url: "https://config.smooai.dev".to_string(),
        auth_url: None, // defaults to SMOOAI_CONFIG_AUTH_URL / https://auth.smoo.ai
        client_id: Some("your-client-id".to_string()),
        api_key: "your-client-secret".to_string(),
        org_id: "your-org-id".to_string(),
        environment: Some("production".to_string()),
        classify: Some(Box::new(move |key, _v| {
            if secret_keys.contains(key) { Classification::Secret }
            else if flag_keys.contains(key) { Classification::Skip }
            else if public_keys.contains(key) { Classification::Public }
            else { Classification::Public }
        })),
    })
    .await?;

    std::fs::write("smoo-config.enc", &result.blob)?;
    println!("SMOO_CONFIG_KEY_FILE=/abs/path/to/smoo-config.enc");
    println!("SMOO_CONFIG_KEY={}", result.key_b64);
    Ok(())
}

Blob env vars

Variable Value
SMOO_CONFIG_KEY_FILE Absolute path to the .enc bundle on disk
SMOO_CONFIG_KEY Base64-encoded 32-byte AES-256 key

Without both set, build_config_runtime returns a plain ConfigManager so dev machines without a baked blob still work — the API stays uniform either way.

The blob format is nonce (12 bytes) || ciphertext || authTag (16 bytes) — wire-identical to the TypeScript, Python, Go, and .NET runtimes. A blob baked in any language decrypts in any other.

Container / Runtime Mode

For long-lived containers (EKS/ECS) the baked blob is the wrong default — when the per-build blob key isn't delivered to the pod, resolution silently falls through to the (absent) file tier and returns an absent value for a required secret (the SMOODEV-1478 CrashLoop outage). Container mode makes the HTTP config API the first-class, fail-loud path: a missing required value is an immediate, typed error (ConfigKeyUnresolvedError), never a silent absent value.

This is the Rust implementation of the five-language parity contract. The env contract, mode selection, fail-loud semantics, caching, and the Kubernetes / External Secrets Operator recipe are documented once in the shared, language-agnostic guide: docs/Container-Runtime-Mode.md.

use smooai_config::container::{init_container_config, ConfigHealth, InitContainerConfigOptions};
use smooai_config::schema::define_config;

let schema = define_config(None, None, None);

// Validates the container env contract (SMOOAI_CONFIG_API_URL / CLIENT_ID /
// CLIENT_SECRET / ORG_ID / ENV), mints an M2M OAuth token, and does an initial
// fetch — auth/network/missing-env failures surface HERE, at startup, not on
// first read. Missing/blank required env => Err(ConfigError::Bootstrap) listing
// exactly which vars are missing.
let handle = init_container_config(InitContainerConfigOptions {
    schema,
    // optional_keys lets specific keys be absent; everything else in the schema
    // is required (container mode's default-required posture).
    optional_keys: vec!["sendgridApiKey".to_string()],
    ..Default::default()
})
.await?;

// Fail-loud read: a required key that resolves absent returns
// Err(ConfigError::KeyUnresolved { key, env, tried_tiers }) — never Ok(None).
let stripe_key = handle.secret_config().get("stripeApiKey").await?;

// Sync read off the cache mirror (same fail-loud contract).
let api_url = handle.public_config().get_sync("apiBaseUrl")?;

// Non-failing status for a Kubernetes readiness/liveness probe (/healthz/config):
// Healthy once the initial fetch succeeded; serves last-good within the 30s cache
// TTL; Unhealthy past hard-expiry on a sustained refresh failure.
match handle.health() {
    ConfigHealth::Healthy => { /* return 200 */ }
    ConfigHealth::Unhealthy { reason } => { /* return 503, log `reason` */ }
}

Mode selection is available as select_mode(...) for callers that need to branch between container mode and the existing blob/file chain (see §2 of the shared doc):

use smooai_config::container::{select_mode, Mode};

// Reads SMOOAI_CONFIG_MODE / M2M creds / blob+file presence from the env.
if select_mode(None) == Mode::Container {
    // build a container-mode handle
}

Defaults match every other SDK: DEFAULT_CACHE_TTL = 30s, DEFAULT_TOKEN_REFRESH_BUFFER_SECONDS = 60. On a 401 the token is invalidated and the request retried once.

Environment Variables

All clients read from the same set of environment variables:

Variable Description Required
SMOOAI_CONFIG_API_URL Base URL of the config API Yes
SMOOAI_CONFIG_CLIENT_ID OAuth2 client ID Yes
SMOOAI_CONFIG_CLIENT_SECRET OAuth2 client secret (legacy SMOOAI_CONFIG_API_KEY accepted as deprecated alias) Yes
SMOOAI_CONFIG_AUTH_URL OAuth issuer base URL (defaults to https://auth.smoo.ai; legacy SMOOAI_AUTH_URL accepted) No
SMOOAI_CONFIG_ORG_ID Organization ID Yes
SMOOAI_CONFIG_ENV Default environment name (defaults to "development") No

Set these in your environment and the client will use them automatically:

export SMOOAI_CONFIG_API_URL="https://config.smooai.dev"
export SMOOAI_CONFIG_CLIENT_ID="your-client-id"
export SMOOAI_CONFIG_CLIENT_SECRET="your-client-secret"
export SMOOAI_CONFIG_ORG_ID="your-org-id"
export SMOOAI_CONFIG_ENV="production"

Configuration Tiers

Tier Purpose Examples
Public Client-visible settings API URLs, feature toggles, UI config
Secret Server-side only Database URLs, API keys, JWT secrets
Feature Flags Runtime toggles A/B tests, gradual rollouts, beta access

Common errors

get_public_config / get_secret_config returning Ok(None) for a known key

If you read a key that wasn't declared in the schema your service was built against, the manager's merged map has no entry and lookups return Ok(None). The common cause is a schema rebase mismatch — the consumer was built against an older schema.json than what's in your config repo. Re-run the schema generator to pick up the new keys, or add the missing key to your schema.

Built With

  • Rust 2021 Edition - Memory safety and performance
  • schemars - JSON Schema generation from Rust types
  • serde / serde_json - JSON serialization
  • reqwest - Async HTTP client
  • tokio - Async runtime

Development

Running tests

cargo test

Building

cargo build --release

Linting and Formatting

cargo clippy
cargo fmt

Related Packages

Contact

Brent Rager

Smoo Github: https://github.com/SmooAI

License

MIT © SmooAI