Loams v0.1.0 is out: run the whole stack yourself with Docker Compose. See the roadmap

Blog/What Loams is built on

Dapr: the API surface, and secrets from every cloud

What Dapr is and how its sidecar and components work, why Loams plans to serve a subset of Dapr's API from a Rust server instead of running a sidecar per function, and how tenant secrets would flow through Dapr's secrets building block.

Dapr: the API surface, and secrets from every cloud
On this page
  1. How Dapr works
  2. Why Loams plans to use it
  3. The proposal: a Dapr API server in Rust, plus one shared daprd
  4. Secrets from every cloud
  5. Limits and open questions

Updated 2026-10-18 for Loams 0.1.0

Changed: confirmed that none of this ships in Loams 0.1.0 (Loams Functions is still planned), and that no loams-dapr server exists in the repository yet.

Planned Loams’ use of Dapr is part of the function runtime proposal (platform part 5). None of it is on the engine's main branch yet, and none of it ships in Loams 0.1.0, where Loams Functions is still Planned.

Dapr, the Distributed Application Runtime, gives applications a set of portable APIs (state, pub/sub, service calls, secrets and more) and lets operators decide what backs each one. Loams’ function runtime plans to use Dapr's API as the interface between user functions and the platform, and Dapr's secrets building block as the one path to tenants' secret stores.

Repositoriesdapr/dapr, dapr/components-contrib
Docsdocs.dapr.io
LicenseApache-2.0
Version checkedv1.18.4
In LoamsPlanned

How Dapr works

Dapr runs next to an application as a sidecar process, daprd, usually one per application instance. The application calls daprd over HTTP or gRPC on localhost, using Dapr's API, and daprd does the work against whatever infrastructure it is configured with.

The API is grouped into building blocks. The current docs list twelve: workflows, service invocation, pub/sub, state, bindings, actors, secrets, configuration, distributed lock, cryptography, jobs and conversation (LLM calls).

What backs a building block is a component, a YAML resource like this:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: vault
spec:
  type: secretstores.hashicorp.vault
  version: v1
  metadata:
    - name: vaultAddr
      value: https://vault.example:8200

The implementations live in components-contrib: dozens of state stores, message brokers, bindings and secret stores behind a common interface per building block. An application written against Dapr's pub/sub API can move from Kafka to NATS by changing a component, not code. Components can be scoped to specific application ids, and secret access can be restricted per application with allow and deny lists.

Why Loams plans to use it

A function runtime needs an API for "talk to the platform": call another function, publish an event, read state, fetch a secret. We could invent one. Dapr's API already exists, has SDKs in every major language, is familiar to many teams and is Apache-2.0. Using its shape means Dapr SDK code that sticks to the supported subset can run on Loams, and gives us a well-trodden answer for the long tail of integrations.

What we do not want is a sidecar per function. Loams’ functions start in milliseconds (workerd isolates, wasmtime components) and thousands can be resident on one node. A Go process beside each one would dwarf them. So the proposal splits Dapr in two.

The proposal: a Dapr API server in Rust, plus one shared daprd

Functions
workerd isolateswasmtime componentsgVisor sandboxes

Each call carries a sandbox token or an mTLS identity.

Rust Dapr server
tenant identityservice invocationpub/sub on Loams streamsstate on Live and TiKVsecrets cacheconfiguration
Shared daprd
NATS, MQTT, AMQP bindingssecret-store components per tenant

One per cluster, off the hot path.

The planned split. Functions reach a Rust server over loopback gRPC; only long-tail bindings and secret stores go to a shared Go daprd.

A new Rust server implements the subset of Dapr's gRPC service (dapr.proto.runtime.v1.Dapr) that maps onto Loams:

Building blockBacked by
Service invocationThe gateway, to another function
Pub/subLoams streams
StateLoams Live or a TiKV keyspace (get and save first; queries later)
BindingsNative for HTTP, cron and Loams streams; everything else forwarded to the shared daprd
SecretsDapr's secrets building block, through the shared daprd (below)
ConfigurationThe tenant's environment in Loams’ control store
WorkflowDenied. Resonate owns durable execution in Loams
Actors, distributed lockOut of scope

Every call carries either an mTLS client certificate with a SPIFFE ID or a per-invocation sandbox token, and the server derives the tenant from that credential, never from a field in the request. It then checks the call with OpenFGA.

The Go daprd stays for what it does best: the long tail of bindings and brokers. It runs once per cluster, and the Rust server forwards those calls to it after the tenant check.

Secrets from every cloud

The owner's decision is to use Dapr's secrets building block as the single path to tenants' secrets. That gives Loams every store in components-contrib without writing a client for each. The maturity levels are Dapr's own:

ComponentStoreDapr status
secretstores.azure.keyvaultAzure Key VaultStable
secretstores.hashicorp.vaultHashiCorp Vault (and OpenBao)Stable
secretstores.kubernetesKubernetes SecretsStable
secretstores.aws.secretmanagerAWS Secrets ManagerBeta
secretstores.aws.parameterstoreAWS SSM Parameter StoreAlpha
secretstores.gcp.secretmanagerGCP Secret ManagerAlpha

Every component implements the same small interface (GetSecret, BulkGetSecret), exposed by the runtime as gRPC calls and as GET /v1.0/secrets/{store}/{key}.

How it would work in Loams:

  1. One component per tenant and store, named after the tenant, carrying the tenant's own credentials, generated by the Loams operator from the tenant's settings. Loams’ own cloud credentials never go into a tenant's component, and no two tenants share one.
  2. Loams enforces the scoping. With one shared daprd there is one Dapr app id, so Dapr's own per-app scoping cannot tell tenants apart. The Rust server takes the tenant from the credential, rewrites the function's logical store name (aws, vault) to that tenant's component, checks the key against the tenant's allow list, and only then calls daprd. A request for another tenant's component cannot even be expressed. Dapr's scopes are still set, as a coarse second layer. Tenants who need Dapr-enforced separation too can get a dedicated daprd.
  3. Off the hot path. Secrets are read at instance start and on cache miss, and cached per tenant, store and key for a short TTL, in memory only, never on disk or in logs.
  4. Delivery per tier. workerd functions get a SECRETS binding whose get(name) is a loopback call; wasmtime components import a host interface; gVisor sandboxes call the standard Dapr HTTP endpoint on their own loopback, so the ordinary Dapr SDKs work.

Limits and open questions

  • Hot reload. Adding a tenant's store must not restart the shared runtime. That depends on Dapr's component hot-reload feature and its maturity in v1.18, which we still have to verify.
  • Workload identity is per pod, not per tenant. An AWS IAM role for service accounts or an Azure workload identity belongs to the daprd pod, so tenant components use their own credentials, Vault auth or cross-account role assumption where supported.
  • A shared daprd holds many tenants' secret-store credentials. Loams-side scoping, per-tenant components, a dedicated daprd on request and keeping daprd off the public network are the mitigations.
  • The Rust server does not exist yet (still true in 0.1.0). An early native stream service and a Rust Dapr app that forwards events from Kafka, agent topics and webhooks into Loams streams exist as uncommitted work; the owner has asked for Resonate, TiKV and Dapr to be validated together in forks before any of it lands in the engine.

The next post covers the three runtimes the function tiers would run on: workerd, wasmtime and gVisor.

More from the blog