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.
Pattern
Section titled “Pattern”flowchart LR Cap[Capability core] --> Trait[Trait boundary] Trait --> A1[Adapter A] Trait --> A2[Adapter B] Trait --> A3[Adapter C] Config[Reactor.toml / features] --> TraitEvery adapter follows the same rules:
- Traits live in library crates —
reactor-core,reactor-cache, capabilitysrc/modules - Adapters implement traits — one module per backend (
fs,s3,embedded,openbao) - Selection is explicit — Cargo features for compile-time; config strings for runtime
- Capabilities never import adapters directly — they depend on
Arc<dyn Trait>
Auth client adapter
Section titled “Auth client adapter”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()));Used by standalone capability binaries in microservices mode. Calls reactor-auth-server over HTTP with an internal secret.
# reactor-data-server configdeployment = "microservices"auth_url = "http://auth.internal:8001"internal_secret = "{{ env REACTOR_INTERNAL_SECRET }}"Vault adapter
Section titled “Vault adapter”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>;}| Adapter | Crate | Use case |
|---|---|---|
EmbeddedVault | reactor-vault | sizes 1–2 — AES-GCM file vault |
OpenBaoVault | reactor-vault | sizes 4–6 — HA secrets cluster |
MockVault | reactor-vault | Tests |
[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"Storage adapter
Section titled “Storage adapter”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.
[storage]backend = "s3"s3_bucket = "my-bucket"s3_region = "us-east-1"s3_endpoint = "https://xxx.r2.cloudflarestorage.com"signing_secret = "hmac-secret"Works with AWS S3, Cloudflare R2, MinIO. Required for sizes 5–6 multi-node (shared state).
Cache backend adapter
Section titled “Cache backend adapter”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.
Function runtime adapter
Section titled “Function runtime adapter”reactor-functions supports multiple runtimes, gated by Cargo features and config:
| Runtime | Feature | Description |
|---|---|---|
| WASM | runtime-wasm | wasmtime sandbox — default, smallest footprint |
| Bun | runtime-bun | Warm-pool JavaScript/TypeScript |
| Lambda | runtime-lambda | AWS Lambda delegation |
[functions]runtimes = ["wasm", "bun"] # subset of compiled runtimesSize 1 (Tauri) builds enable runtime-wasm only — dropping Bun and the AWS SDK saves ~20 MB.
LLM provider adapter
Section titled “LLM provider adapter”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;}| Adapter | Config key | Provider |
|---|---|---|
OpenRouterClient | openrouter_api_key | OpenRouter |
BedrockClient | aws_* keys | AWS Bedrock |
FoundryClient | azure_foundry_* | Azure AI Foundry |
OpenAiCompatibleClient | custom endpoint | Any OpenAI-compatible API |
The gateway registry maps aliases (e.g. fast, power) to upstream models per provider.
Connect runtime adapter
Section titled “Connect runtime adapter”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>;}| Runtime | Kind | Description |
|---|---|---|
NativeRuntime | native | First-party Rust connectors |
ManifestRuntime | manifest | Airbyte Low-Code CDK YAML interpreter |
| Airbyte container | airbyte | Runs via reactor-jobs worker |
Nothing outside the runtime trait touches a connector directly — this is the load-bearing abstraction.
Sites framework adapter
Section titled “Sites framework adapter”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.
Cloud provider adapter
Section titled “Cloud provider adapter”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>;}| Provider | Status | Target |
|---|---|---|
FlyProvider | v0 | Fly.io machines |
| AWS | planned | ECS/Fargate |
| GCP | planned | Cloud Run |
Shared cluster adapters (C6@fly)
Section titled “Shared cluster adapters (C6@fly)”Multi-tenant mode adds tenant-scoped adapters:
| Component | In-process | Shared cluster |
|---|---|---|
| Realtime | memory broadcast | NATS JetStream |
| PubSub | memory channels | NATS |
| Database | direct pool | Supavisor transaction pool |
| Tenant resolution | N/A | host-based cache + cold load |
[cloud]multi_tenant = trueprovider = "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 = 5Testing with mock adapters
Section titled “Testing with mock adapters”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 statelet vault = Arc::new(MockVault::new());let cache = Arc::new(InMemoryCache::new());This keeps tests honest: they exercise the same adapter boundaries production code uses.
Related
Section titled “Related”- Architecture overview — workspace and capability model
- Configuration — adapter selection via config
- Deployment grades — which adapters each grade uses