Skip to content

SDK Overview

SecretSpec ships SDKs for Rust, Python, Go, Ruby, Node.js/TypeScript, and Haskell. They all resolve secrets from the same declarative secretspec.toml, and they all behave identically, because they share one resolver.

Resolution (providers, fallback chains, profiles, generation, as_path materialization) lives in a single Rust core. Each SDK is a thin client over that core rather than a reimplementation:

  • Rust uses the library directly, with a compile-time derive macro for strongly-typed access.
  • Ruby (a native C extension) statically links the secretspec-ffi C ABI at build time; Go (purego) loads it at runtime with no cgo. Both exchange a small JSON request/response with the core.
  • Haskell links the same C ABI at build time via the GHC FFI.
  • Python uses a pyo3 native extension, and Node.js/TypeScript uses a napi-rs native addon; both embed the same resolver directly and exchange the same JSON request/response shape as the C ABI.

Because resolution happens in one place, every provider, chain, profile, and generator works the same in every language, and a new provider added to the core is immediately available everywhere with no per-SDK change. A cross-language conformance suite asserts that all the SDKs reduce the same inputs to the same result.

Each SDK mirrors the Rust derive crate’s vocabulary: a builder that takes a provider, profile, and an access reason, and a load/resolve that returns the resolved secrets plus the provider and profile used. A missing required secret is a typed error, distinct from a transport failure (which carries a stable kind). Secrets exposed as_path come back as a readable file path.

from secretspec import SecretSpec
resolved = SecretSpec.builder().with_provider("keyring://").with_reason("boot").load()
print(resolved.secrets["DATABASE_URL"].get)

See each language’s page for the idiomatic spelling: Rust, Python, Go, Ruby, Node.js, and Haskell.

Beyond the Rust derive macro, typed accessors for the other languages are generated from the manifest. secretspec schema emits a JSON Schema for the secret shape; quicktype turns it into an idiomatic type and deserializer for any language, which you build from the SDK’s fields() map:

Terminal window
secretspec schema | quicktype -s schema --top-level SecretSpec --lang <language>

This keeps the per-language surface tiny: the SDK only provides fields(), and quicktype owns the type generation.

The resolver ships inside each package, so there is nothing extra to install and no runtime library path to set:

  • Python builds the resolver into a pyo3 extension shipped as a cp39-abi3 wheel, and Ruby statically links the secretspec-ffi archive into a native C extension in the gem.
  • Haskell statically links the same archive at build time via the GHC FFI.
  • Go embeds the cdylib in the module and loads it at runtime via purego (no cgo); an opt-in -tags static build links it statically instead.
  • Node.js builds the resolver into a napi-rs addon.

Because the resolver is linked or embedded directly, none of the SDKs depend on a separately installed cdylib or an LD_LIBRARY_PATH/SECRETSPEC_FFI_LIB override at runtime.