Loam is pre-alpha: the engine core runs today; Live and Durable are in progress. See the roadmap

Blog/What Loam is built on

Dapr: the API surface, and secrets from every cloud

What Dapr is and how its sidecar and components work, why Loam 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 Loam 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

Planned Loam's use of Dapr is part of the function runtime proposal (platform part 5). None of it is on the engine's main branch yet.

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. Loam's 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 LoamPlanned

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 Loam 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 Loam, and gives us a well-trodden answer for the long tail of integrations.

What we do not want is a sidecar per function. Loam's 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 Loam 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 Loam:

Building blockBacked by
Service invocationThe gateway, to another function
Pub/subLoam streams
StateLoam Live or a TiKV keyspace (get and save first; queries later)
BindingsNative for HTTP, cron and Loam streams; everything else forwarded to the shared daprd
SecretsDapr's secrets building block, through the shared daprd (below)
ConfigurationThe tenant's environment in Loam's control store
WorkflowDenied. Resonate owns durable execution in Loam
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 Loam 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 Loam:

  1. One component per tenant and store, named after the tenant, carrying the tenant's own credentials, generated by the Loam operator from the tenant's settings. Loam's own cloud credentials never go into a tenant's component, and no two tenants share one.
  2. Loam 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. Loam-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. An early native stream service and a Rust Dapr app that forwards events from Kafka, agent topics and webhooks into Loam 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