Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

Adapters

Reactor avoids hard-coding infrastructure dependencies. Instead, each integration point defines a trait (Rust interface) with one or more adapter implementations selected at compile time (Cargo features) or runtime (config). This keeps the core capability logic stable while letting you swap Postgres pooling strategies, object storage backends, LLM providers, and connector runtimes.

flowchart LR
Cap[Capability core] --> Trait[Trait boundary]
Trait --> A1[Adapter A]
Trait --> A2[Adapter B]
Trait --> A3[Adapter C]
Config[Reactor.toml / features] --> Trait

Every adapter follows the same rules:

  1. Traits live in library cratesreactor-core, reactor-cache, capability src/ modules
  2. Adapters implement traits — one module per backend (fs, s3, embedded, openbao)
  3. Selection is explicit — Cargo features for compile-time; config strings for runtime
  4. Capabilities never import adapters directly — they depend on Arc<dyn Trait>

Capabilities need to verify JWTs. The AuthClient trait abstracts whether auth runs in-process or over HTTP.

// reactor-core — simplified
#[async_trait]
pub trait AuthClient: Send + Sync {
async fn verify_token(&self, token: &str) -> Result<Claims, AuthError>;
async fn get_user(&self, id: &UserId) -> Result<User, AuthError>;
}

Used by reactor-server. Calls AuthService directly — no HTTP, no JWT round-trip across loopback.

let auth_client = Arc::new(
InProcessAuthClient::new(auth_state.service.clone())
);

Secrets are accessed through the Vault trait from reactor-core:

#[async_trait]
pub trait Vault: Send + Sync {
async fn get_secret(&self, path: &str) -> Result<SecretValue, VaultError>;
async fn put_secret(&self, path: &str, value: &SecretValue) -> Result<(), VaultError>;
async fn delete_secret(&self, path: &str) -> Result<(), VaultError>;
}
AdapterCrateUse case
EmbeddedVaultreactor-vaultsizes 1–2 — AES-GCM file vault
OpenBaoVaultreactor-vaultsizes 4–6 — HA secrets cluster
MockVaultreactor-vaultTests
[vault]
backend = "embedded" # or "openbao"
path = ".reactor/vault"
master_key = "env:REACTOR_VAULT_MASTER_KEY"

Config values reference vault paths with the vault: prefix:

[ai]
openrouter_api_key = "vault:ai/openrouter"

Object storage backends implement a common interface inside reactor-storage:

[storage]
backend = "fs"
fs_base_path = "./.reactor/blobs"
signing_secret = "hmac-secret"

Best for sizes 1–2 single-node. Blobs live on disk; signed URLs use HMAC.


reactor-cache defines CacheBackend — combined KV and queue operations:

#[async_trait]
pub trait CacheBackend: QueueOperations + KvOperations + Send + Sync {
async fn migrate(&self) -> Result<(), CacheError>;
async fn health_check(&self) -> Result<(), CacheError>;
}

Implementations include in-memory (dev/test) and Postgres-backed (production). The unified server injects Arc<dyn CacheBackend> into SharedResources.


reactor-functions supports multiple runtimes, gated by Cargo features and config:

RuntimeFeatureDescription
WASMruntime-wasmwasmtime sandbox — default, smallest footprint
Bunruntime-bunWarm-pool JavaScript/TypeScript
Lambdaruntime-lambdaAWS Lambda delegation
[functions]
runtimes = ["wasm", "bun"] # subset of compiled runtimes

Size 1 (Tauri) builds enable runtime-wasm only — dropping Bun and the AWS SDK saves ~20 MB.


reactor-ai routes chat completions through the ChatProvider trait:

#[async_trait]
pub trait ChatProvider: Send + Sync {
async fn chat_completion(&self, request: &ChatCompletionRequest, model: &str)
-> Result<(ChatCompletionResponse, Duration), AiError>;
async fn chat_completion_stream(&self, request: &ChatCompletionRequest, model: &str)
-> Result<(Stream, Instant), AiError>;
async fn embeddings(&self, request: &EmbeddingsRequest, model: &str)
-> Result<(EmbeddingsResponse, Duration), AiError>;
fn name(&self) -> &'static str;
}
AdapterConfig keyProvider
OpenRouterClientopenrouter_api_keyOpenRouter
BedrockClientaws_* keysAWS Bedrock
FoundryClientazure_foundry_*Azure AI Foundry
OpenAiCompatibleClientcustom endpointAny OpenAI-compatible API

The gateway registry maps aliases (e.g. fast, power) to upstream models per provider.


reactor-connect syncs data from third-party sources via ConnectorRuntime:

#[async_trait]
pub trait ConnectorRuntime: Send + Sync + 'static {
fn kind(&self) -> RuntimeKind;
async fn spec(&self) -> Result<ConnectorDescriptor, ConnectError>;
async fn check(&self, config: &Value) -> Result<ConnectionStatus, ConnectError>;
async fn discover(&self, config: &Value) -> Result<DiscoveredCatalog, ConnectError>;
async fn read(&self, config: &Value, catalog: &ConfiguredCatalog, state: &StateBundle)
-> Result<MessageStream, ConnectError>;
async fn write(&self, config: &Value, catalog: &ConfiguredCatalog, messages: MessageStream)
-> Result<WriteOutcome, ConnectError>;
}
RuntimeKindDescription
NativeRuntimenativeFirst-party Rust connectors
ManifestRuntimemanifestAirbyte Low-Code CDK YAML interpreter
Airbyte containerairbyteRuns via reactor-jobs worker

Nothing outside the runtime trait touches a connector directly — this is the load-bearing abstraction.


Build-time adapters detect and build frontend projects:

#[async_trait]
pub trait FrameworkAdapter: Send + Sync {
fn name(&self) -> Framework;
fn detect(&self, project_dir: &Path) -> bool;
async fn build(&self, project_dir: &Path, opts: &BuildOpts) -> Result<SiteBundle, SitesError>;
}

Implementations: Astro, Next.js, static HTML. The CLI runs reactor sites build which selects the adapter by detection order.


The control plane (reactor-cloud) abstracts infrastructure provisioning:

#[async_trait]
pub trait CloudProvider: Send + Sync {
async fn provision(&self, req: ProvisionRequest) -> Result<ProvisionResult, ProviderError>;
async fn deploy(&self, req: DeployRequest) -> Result<DeployResult, ProviderError>;
async fn teardown(&self, project_id: &str) -> Result<(), ProviderError>;
async fn status(&self, project_id: &str) -> Result<ProjectStatus, ProviderError>;
async fn logs(&self, project_id: &str, opts: LogOptions) -> Result<LogStream, ProviderError>;
async fn set_secrets(&self, project_id: &str, secrets: HashMap<String, String>) -> Result<(), ProviderError>;
}
ProviderStatusTarget
FlyProviderv0Fly.io machines
AWSplannedECS/Fargate
GCPplannedCloud Run

Multi-tenant mode adds tenant-scoped adapters:

ComponentIn-processShared cluster
Realtimememory broadcastNATS JetStream
PubSubmemory channelsNATS
Databasedirect poolSupavisor transaction pool
Tenant resolutionN/Ahost-based cache + cold load
[cloud]
multi_tenant = true
provider = "shared_cluster"
[cloud.realtime]
backend = "nats"
nats = { servers = ["nats://nats-0.internal:4222"] }
[cloud.shared_pool]
shared_postgres_url = "postgres://admin@shared-pg.internal/postgres"
per_tenant_pool_size = 5

Every trait has a mock implementation for unit and composition tests:

// Composition tests boot reactor-server with testcontainers Postgres
// and reach the system only through HTTP — never hand-construct capability state
let vault = Arc::new(MockVault::new());
let cache = Arc::new(InMemoryCache::new());

This keeps tests honest: they exercise the same adapter boundaries production code uses.