Storage Backends
FlowOrchestrator's persistence layer is built on four core interfaces. The package you install provides an implementation; you can also swap in your own.
Core Interfaces
| Interface | Responsibility |
|---|---|
IFlowStore |
Flow definitions and enabled/disabled state |
IFlowRunStore |
Run records and step status tracking |
IOutputsRepository |
Step input/output blobs keyed by (RunId, StepKey) |
IFlowRepository |
In-process registry of the code-defined IFlowDefinition classes; validated by AddFlowOrchestrator at startup |
These four interfaces are the minimum required to replace the built-in backends — AddFlowOrchestrator throws at startup if either IFlowStore or IFlowRepository is missing.
SQL Server
dotnet add package FlowOrchestrator.SqlServer
builder.Services.AddFlowOrchestrator(options =>
{
options.UseSqlServer(connectionString);
options.UseHangfire();
});
FlowOrchestratorSqlMigrator runs on startup and auto-creates all required tables if they do not exist. No manual migration step is needed.
Tables created:
| Table | Purpose |
|---|---|
FlowDefinitions |
Registered flows, enable/disable state |
FlowRuns |
One row per run: status, timestamps, trigger key |
FlowSteps |
One row per step per run: status, attempt count |
FlowStepAttempts |
Detailed per-attempt records (start, end, error) |
FlowOutputs |
Step inputs and outputs serialized as JSON |
FlowStepDispatches |
Idempotent dispatch ledger — supports the Dispatch many, Execute once invariant |
FlowStepClaims |
Exclusive execution claim per step (the Execute once half) |
FlowRunControls |
Cancellation, timeout, and idempotency key records |
FlowIdempotencyKeys |
Trigger-time idempotency dedupe |
FlowEvents |
Event stream records (when EnableEventPersistence = true) |
FlowSignalWaiters |
Parked WaitForSignal step state (see WaitForSignal) |
FlowScheduleStates |
Cron override storage (when Scheduler.PersistOverrides = true) |
WebhookReplayNonces |
Replay-attack nonce ledger for the dashboard webhook hardening pipeline (v1.25; opt-in) |
WebhookRejections |
DLQ + recent-deliveries log for the webhook pipeline (v1.25; opt-in) |
Connection string format:
{
"ConnectionStrings": {
"FlowOrchestrator": "Server=.;Database=FlowOrchestrator;Trusted_Connection=True;"
}
}
PostgreSQL
dotnet add package FlowOrchestrator.PostgreSQL
builder.Services.AddFlowOrchestrator(options =>
{
options.UsePostgreSql(connectionString);
options.UseHangfire();
});
PostgreSqlFlowOrchestratorMigrator creates the same table set in PostgreSQL on startup. Uses Npgsql — no EF Core dependency. PostgreSQL table names are snake_case (flow_runs, webhook_replay_nonces, webhook_rejections, …).
The migrator also makes a best-effort attempt to enable the pg_trgm
extension and create GIN trigram indexes that accelerate the dashboard run
search (?search=) — substring ILIKE matching on flow_name, trigger_key,
and the flow_steps step_key / error_message / output_json columns. If
the connection role lacks the CREATE EXTENSION privilege the step is skipped
with a warning and search still works via a sequential scan; grant the privilege
(or pre-create the extension) to keep search fast on large histories.
Webhook hardening backends (v1.25)
The replay-nonce + DLQ stores default to in-memory (single-replica only). For multi-replica deployments register the backend-specific implementations:
builder.Services.AddFlowOrchestrator(options =>
{
options.UseSqlServer(sqlConn);
options.AddSqlServerWebhookHardening(sqlConn); // SqlWebhookReplayStore + SqlWebhookRejectionStore
});
// or for PostgreSQL:
builder.Services.AddFlowOrchestrator(options =>
{
options.UsePostgreSql(pgConn);
options.AddPostgreSqlWebhookHardening(pgConn); // PostgreSqlWebhookReplayStore + PostgreSqlWebhookRejectionStore
});
Both replay-store implementations use atomic upserts (INSERT … WHERE NOT EXISTS on Sql Server, ON CONFLICT DO NOTHING on Postgres) so concurrent replicas race correctly without a serialisable transaction. Tables are created by the existing migrators with idempotent IF NOT EXISTS guards.
{
"ConnectionStrings": {
"FlowOrchestratorPg": "Host=localhost;Database=floworch;Username=app;Password=secret"
}
}
In-Memory
dotnet add package FlowOrchestrator.InMemory
builder.Services.AddFlowOrchestrator(options =>
{
options.UseInMemory();
options.UseHangfire();
});
Warning
All run data is lost when the process restarts. Use this for local development and unit tests only.
UseInMemory() must be called explicitly — there is no silent fallback. Calling AddFlowOrchestrator() without any storage backend throws InvalidOperationException on startup.
Comparing Backends
| Feature | SQL Server | PostgreSQL | In-Memory |
|---|---|---|---|
| Persistence across restarts | Yes | Yes | No |
| Run history and step timeline | Yes | Yes | Yes (current session) |
| Schedule override persistence | Yes | Yes | No |
| Cron expressions | Yes | Yes | Yes |
| Webhook deduplication | Yes | Yes | Session only |
| Testcontainers support | Yes | Yes | — |
| Production-ready | Yes | Yes | No |
Custom Backend
Implement the four core interfaces:
public sealed class RedisFlowStore : IFlowStore { ... }
public sealed class RedisFlowRunStore : IFlowRunStore { ... }
public sealed class RedisOutputsRepository : IOutputsRepository { ... }
public sealed class RedisFlowRepository : IFlowRepository { ... }
Register them directly on options.Services:
builder.Services.AddFlowOrchestrator(options =>
{
options.Services.AddSingleton<IFlowStore, RedisFlowStore>();
options.Services.AddSingleton<IFlowRunStore, RedisFlowRunStore>();
options.Services.AddSingleton<IOutputsRepository, RedisOutputsRepository>();
options.Services.AddSingleton<IFlowRepository, RedisFlowRepository>();
options.UseHangfire();
});
Advanced Contracts
For full feature parity (schedule overrides, run control, event stream, retention, and concurrency safety), implement these additional interfaces:
| Interface | Feature |
|---|---|
IFlowScheduleStateStore |
Persistent cron overrides (Scheduler.PersistOverrides) |
IFlowRunControlStore |
Cancel, timeout, and idempotency key state. The engine checks this on every TriggerAsync to deduplicate runs and on every RunStepAsync to honour cancellation. |
IFlowEventReader |
Run event stream (GET /flows/api/runs/{runId}/events) |
IFlowRetentionStore |
Background retention sweep. Nothing cascades — every per-run table must be deleted explicitly, including FlowStepDispatches and FlowSignalWaiters, which carry no foreign key to FlowRuns. See Observability — Retention for the full table list. |
IFlowSignalStore |
Parked WaitForSignal waiter state. Required if you want to use the WaitForSignal built-in step on a custom backend. |
IFlowRunRuntimeStore |
Step claim/dispatch ledger per run. Implements TryRecordDispatchAsync (idempotent INSERT — prevents duplicate dispatch) and TryClaimStepAsync (claim exclusion — ensures a step is executed by at most one worker). Required for production use with any multi-worker runtime. |
Register these the same way — directly on options.Services.
Members with default implementations — override them
Two interface members ship with default implementations so that a store written against an earlier version still compiles. Both defaults are correct and both are slow, and neither will warn you:
| Member | Default | Why override |
|---|---|---|
IFlowRunRuntimeStore.IsStepClaimedAsync(runId, stepKey) |
Calls GetClaimedStepKeysAsync and scans the result |
Claims are deliberately never released on terminal status, so the claim set holds one key per executed step and grows with the run. The default therefore fetches every key to answer a single-key question — on every signal delivery. Override with a point lookup on the (RunId, StepKey) primary key. |
IFlowRunStore.GetRunAsync(runId) |
Calls GetRunDetailAsync |
GetRunDetailAsync issues three queries and returns every step and attempt row including their unbounded JSON columns. Callers that need only the run header — the signal dispatcher, several dashboard endpoints — then pay for the whole graph. On SQL Server this read has been observed to time out on a 150-iteration ForEach under load. Override with a single-row read. |
The built-in SQL Server, PostgreSQL and in-memory stores override both.
Hangfire Storage vs FlowOrchestrator Storage
These are completely independent stores:
- Hangfire storage — holds job queue, server heartbeats, retry state. Configured on
AddHangfire(...). - FlowOrchestrator storage — holds flow definitions, run history, step outputs. Configured on
options.UseSqlServer(...)/UsePostgreSql()/UseInMemory().
You can mix them freely:
// Hangfire on SQL Server, FlowOrchestrator on PostgreSQL
builder.Services.AddHangfire(c => c.UseSqlServerStorage(sqlConnStr));
builder.Services.AddFlowOrchestrator(options =>
{
options.UsePostgreSql(pgConnStr);
options.UseHangfire();
});