Policies¶
OpenPit exposes two policy stages:
Start stage: cheap checks that must see every request and do not need rollback support.Main stage: deeper checks that may emit multiple rejects or register reversible mutations.
Exact interface names differ by SDK and are listed in the language-specific sections below.
Configuration Model¶
Built-in policies use an axis-based configuration model. Each policy has one or more axes - dimensions along which a limit or barrier is applied. An axis becomes active only when a barrier is explicitly configured for it.
Common axes across built-in policies:
- Broker axis: applies to all orders regardless of account or asset.
- Asset axis: applies to all orders with a given settlement asset.
- Account axis: applies to all orders from a given account.
- Account+Asset axis: applies to orders from a specific account and settlement asset pair.
An axis without a matching barrier never rejects the order. At least one axis must be configured; constructors return an error if all axes are empty.
Synchronization¶
The sync policy is chosen on the engine builder. It controls how stateful built-in policies synchronize their internal storage and defines the engine handle's threading capability (see Threading Contract for the per-mode contract).
| Mode | Behavior | Use case |
|---|---|---|
| Full sync | Thread-safe handle; concurrent invocation on the same handle is safe. | Engine is shared across threads, or migration patterns make sequential pinning impractical. |
| No sync | Single-threaded handle; calls must stay on the OS thread that created the engine. | Single-threaded embedding; zero synchronization overhead. |
| Account sync | Sequential cross-thread access; caller pins each account to one processing chain. | Account-sharded workloads where one worker / queue owns each account. |
Storage owned by a built-in or custom policy is always account-keyed
when used through binding-facing storage contracts: the top-level key
carries an account id, and the per-account value can be any structure the
policy needs. The choice of sync mode does not change this rule.
Method names per language:
Go
- `openpit.NewEngineBuilder().FullSync()` - `openpit.NewEngineBuilder().NoSync()` - `openpit.NewEngineBuilder().AccountSync()` If you are unsure, start with `FullSync()`: goroutines migrate between OS threads at any await point, so even a single goroutine calling the engine sequentially can wake up on a different OS thread than the one it suspended on. `FullSync()` is the safe default. `AccountSync()` only requires that calls for the same account are never concurrent - it does not require a fixed OS thread. The standard Go pattern is a sharded worker pool: hash the account ID to one of N channels; one goroutine drains each channel, so the same account always lands on the same worker.const shards = 256
type task struct {
accountID string
order model.Order
}
// One channel per shard, one goroutine draining each channel: the same
// account always lands on the same worker, so AccountSync holds.
workers := make([]chan task, shards)
for i := range workers {
ch := make(chan task, 1024)
workers[i] = ch
go func(ch <-chan task) {
for t := range ch {
req, _, _ := engine.ExecutePreTrade(t.order)
if req != nil {
req.Close()
}
}
}(ch)
}
// fnv32 hashes the account ID; the modulo pins each account to one shard.
dispatch := func(t task) {
workers[fnv32(t.accountID)%shards] <- t
}
Python
- `openpit.Engine.builder().full_sync()` - `openpit.Engine.builder().no_sync()` - `openpit.Engine.builder().account_sync()` Prefer `no_sync()` when you do not explicitly work with multiple threads yourself - it has zero synchronization overhead and is the right default for embeddings that drive the engine from a single thread (synchronous code or one asyncio loop). Use `full_sync()` only when you actually share the engine across threads concurrently. Use `account_sync()` for sharded sequential workloads where each account is pinned to one processing chain.C++
- `openpit::EngineBuilder(openpit::SyncPolicy::Full)` - `openpit::EngineBuilder(openpit::SyncPolicy::None)` - `openpit::EngineBuilder(openpit::SyncPolicy::Account)` The sync policy is a constructor argument on `openpit::EngineBuilder`; the SDK does not impose a default. Prefer `SyncPolicy::None` for a single-threaded embedding with zero synchronization overhead. Use `SyncPolicy::Full` only when the engine handle is actually shared across threads concurrently, and `SyncPolicy::Account` for sharded sequential workloads where each account is pinned to one processing chain.Rust
- `Engine::builder::Account Blocking by Engine¶
The engine maintains an internal blocked-accounts registry next to the
policy chain. When a policy emits a reject with scope = account or signals
a kill switch from apply execution report, the engine records the affected
account and short-circuits every subsequent pre-trade request for that
account before any policy runs.
The block is owned by the engine, not by the policy. A policy must not keep its own "this account is blocked" flag and must not re-check the condition on every order - returning the reject once is the entire contract. Once a block is recorded, the engine guarantees the policy is not invoked again for that account until the block is cleared.
When the engine records a block¶
- A
start stagecallback returns rejects that includescope = account. - A
main stagecallback returns rejects that includescope = account. apply execution reportreturns a non-empty list ofAccountBlockvalues (a kill switch).
Caution: returning a Reject with scope = account from any pre-trade
stage triggers an engine-recorded account block. A policy that intends to
reject only the current order, for example for a transient condition that
may resolve on its own, must use scope = order instead. An engine-recorded
block can be lifted explicitly via accounts().unblock(account) without
rebuilding the engine; see Account Blocking.
Only the first AccountBlock recorded for an account is kept. Later
recordings for the same account are no-ops - first cause wins. When
apply execution report is dispatched across policies, the engine records
only the first AccountBlock from the first policy that returned a non-empty
list. Remaining blocks, whether from the same policy or from later policies,
are still returned to the caller in PostTradeResult account blocks but do
not affect the registry.
What gets blocked¶
- If the offending order or report exposes an
account id, only that account is blocked, across every settlement asset and instrument. - If the order or report cannot produce an
account id, the engine activates a global block and rejects every subsequent pre-trade request, regardless of account.
What the caller observes¶
- For blocks triggered by start-stage or main-stage rejects, future rejects on
that account carry
code = AccountBlockedand replay thepolicy,reason, anddetailsfrom the original reject. - For blocks triggered by
apply execution report, future rejects replay thecode,reason, anddetailsfrom theAccountBlockthe policy emitted (for examplePnlKillSwitchTriggered). - While any block is active and an order arrives without an
account id, the engine rejects it withMissingRequiredFieldand the order scope.
When a block is cleared¶
A blocked account stays blocked until it is explicitly unblocked or the engine
is rebuilt. There is no implicit unblock: rerunning the original check,
replaying the order, retrying the request, or letting time pass does not lift
the block. A block recorded by the engine can also be lifted explicitly without
a rebuild by calling accounts().unblock(account) on the engine's accounts
handle; see Account Blocking for the full admin API.
Built-in Policies¶
SpotFundsPolicy¶
It tracks per-account spendable funds and rejects orders an account cannot
afford. Each (account, asset) slot holds three buckets - available, held,
and incoming. A reserve moves the outflow amount from available to held
and simultaneously records the expected inflow on the acquiring leg as
incoming (buy: base asset quantity; priced sell: settlement proceeds). A fill
consumes held and credits the acquired asset to available, reducing
incoming by the filled amount. A cancel releases the unfilled remainder from
both held and incoming. The incoming bucket is informational only and
never gates orders. Balances are seeded only through the
account adjustment pipeline.
For configuration depth - market-order pricing, slippage, per-instrument overrides, and the holdings lifecycle - see the dedicated Spot Funds page. To keep your own ledger aligned with the funds the policy moves, see Balance Reconciliation.
What it controls:
- Rejects orders whose cost exceeds
availableplusheldfunds - Prices market orders from a market data bundle when configured
- Optionally blocks an account when its engine-computed realized PnL moves outside a configured bound on the account-currency axis - see Spot Funds - Self-Computed PnL Kill Switch
Required fields to populate:
Order:instrument,account id,side,trade amount, optionalpriceExecution Report:instrument,account id,side, fill detailsAccount Adjustment:balance operation,amount,bounds
Rejects:
- Insufficient spendable funds
Code:
InsufficientFunds, Scope:order - Market order with no market-data bundle configured
Code:
UnsupportedOrderType, Scope:order - Market order priced from a stale or missing quote field; or a sell order with
no order price and no market-data price
Code:
MarkPriceUnavailable, Scope:order - Execution report (fill or cancel) arrives without its lock price
Code:
MissingRequiredField, Scope:account
Python
import openpit
import openpit.pretrade.policies
# Limit-only spot funds, registered first in the policy list.
engine = (
openpit.Engine.builder()
.no_sync()
.builtin(openpit.pretrade.policies.build_spot_funds())
.build()
)
C++
namespace policies = openpit::pretrade::policies;
openpit::EngineBuilder builder(openpit::SyncPolicy::None);
// Limit-only mode: no market-data service handle.
builder.Add(policies::SpotFundsPolicy{});
const openpit::Engine engine = builder.Build();
Rust
use openpit::pretrade::policies::{SpotFundsPolicy, SpotFundsSettings};
use openpit::{
Engine, FullSync, OrderOperation, SpotFundsMarketData, SpotFundsPricingSource,
WithAccountAdjustmentAmount, WithAccountAdjustmentBalanceOperation,
WithAccountAdjustmentBounds, WithExecutionReportFillDetails, WithExecutionReportOperation,
};
// Report and account-adjustment shapes composed from public SDK wrappers.
type SpotReport = WithExecutionReportOperation<WithExecutionReportFillDetails<()>>;
type SpotAdjustment = WithAccountAdjustmentAmount<
WithAccountAdjustmentBounds<WithAccountAdjustmentBalanceOperation<()>>,
>;
let builder = Engine::builder::<OrderOperation, SpotReport, SpotAdjustment>().full_sync();
// Limit-only mode: no market-data bundle.
let policy = SpotFundsPolicy::<FullSync, FullSync>::new(
SpotFundsSettings::new(0, SpotFundsPricingSource::Mark, [])?,
None::<SpotFundsMarketData<FullSync>>,
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
OrderValidationPolicy¶
Validates basic order structure before more expensive checks. Does not validate price, side, instrument, or account.
What it controls:
- Rejects zero quantity
trade amount - Rejects zero volume
trade amount
Required fields to populate:
Order:trade amountExecution Report: none
Rejects:
- Missing
trade amountCode:MissingRequiredField, Scope:orderReason:failed to access required field - Zero quantity
Code:
InvalidFieldValue, Scope:orderReason:order quantity must be non-zero - Zero volume
Code:
InvalidFieldValue, Scope:orderReason:order volume must be non-zero
Go
engine, err := openpit.NewEngineBuilder().
NoSync().
Builtin(
policies.BuildOrderValidation(),
).
Build()
Python
import openpit
import openpit.pretrade.policies
engine = (
openpit.Engine.builder()
.no_sync()
.builtin(
openpit.pretrade.policies.build_order_validation(),
)
.build()
)
C++
namespace policies = openpit::pretrade::policies;
openpit::EngineBuilder builder(openpit::SyncPolicy::None);
builder.Add(policies::OrderValidationPolicy{});
const openpit::Engine engine = builder.Build();
Rust
use openpit::pretrade::policies::OrderValidationPolicy;
use openpit::{Engine, OrderOperation};
let engine = Engine::builder::<OrderOperation, (), ()>()
.no_sync()
.pre_trade(OrderValidationPolicy::new())
.build()?;
RateLimitPolicy¶
Counts start-stage attempts inside a time window. Flood semantics: rejected attempts count against the limit to prevent bypass by retrying.
What it controls (by axis):
- Broker axis: global limit across all orders. Uses an approximate fixed-window counter.
- Asset axis: limit per settlement asset. Uses an approximate fixed-window counter.
- Account axis: limit per account. Uses a precise sliding-window log.
- Account+Asset axis: limit per account and settlement asset pair. Uses a precise sliding-window log.
Caution: RateLimitAccountBarrier and RateLimitAccountAssetBarrier
reject with scope = account on breach. The engine records the affected
account in its blocked-accounts registry on the first breach, which turns
what would otherwise be a window-bound rate limit into an account block that
persists until explicitly cleared. Choose these axes only when that escalation
is intended; use the broker or asset axes (both reject with scope = order)
when a transient cap is enough. A block recorded this way can be lifted via
accounts().unblock(account) without rebuilding the engine.
All configured axes are incremented and checked on every call. Checks run in order: broker -> asset -> account -> account+asset.
Limit parameters (RateLimit):
| Field | Type | Description |
|---|---|---|
max orders |
integer | maximum attempts in the window |
window |
duration | length of the time window |
Required fields to populate:
Order:instrument(when asset or account+asset axes are configured),account id(when account or account+asset axes are configured)Execution Report: none
Rejects:
- Code:
RateLimitExceeded, Scope:orderReason:rate limit exceeded: broker barrier - Code:
RateLimitExceeded, Scope:orderReason:rate limit exceeded: asset barrier - Code:
RateLimitExceeded, Scope:accountReason:rate limit exceeded: account barrier - Code:
RateLimitExceeded, Scope:accountReason:rate limit exceeded: account+asset barrier - Code:
MissingRequiredField, Scope:order(when a required field cannot be read)
Go
engine, err := openpit.NewEngineBuilder().
NoSync().
Builtin(
policies.BuildRateLimit().
BrokerBarrier(
policies.RateLimitBrokerBarrier{
Limit: policies.RateLimit{
MaxOrders: 100,
Window: time.Second,
},
},
),
).
Build()
Python
import datetime
import openpit
import openpit.pretrade.policies
engine = (
openpit.Engine.builder()
.no_sync()
.builtin(
openpit.pretrade.policies.build_rate_limit()
.broker_barrier(
openpit.pretrade.policies.RateLimitBrokerBarrier(
limit=openpit.pretrade.policies.RateLimit(
max_orders=100,
window=datetime.timedelta(seconds=1),
),
),
)
)
.build()
)
C++
namespace policies = openpit::pretrade::policies;
openpit::EngineBuilder builder(openpit::SyncPolicy::None);
builder.Add(policies::RateLimitPolicy{}.BrokerBarrier(
policies::RateLimitBrokerBarrier(policies::RateLimit(
/*maxOrders=*/100, /*windowNanoseconds=*/1'000'000'000))));
const openpit::Engine engine = builder.Build();
Rust
use std::time::Duration;
use openpit::pretrade::policies::{
RateLimit,
RateLimitBrokerBarrier,
RateLimitPolicy,
RateLimitSettings,
};
use openpit::{
Engine, OrderOperation, WithExecutionReportOperation, WithFinancialImpact,
};
type Report = WithExecutionReportOperation<WithFinancialImpact<()>>;
let builder = Engine::builder::<OrderOperation, Report, ()>().no_sync();
let policy = RateLimitPolicy::new(
RateLimitSettings::new(
Some(RateLimitBrokerBarrier {
limit: RateLimit {
max_orders: 100,
window: Duration::from_secs(1),
},
}),
[], // asset barriers
[], // account barriers
[], // account+asset barriers
)?,
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
OrderSizeLimitPolicy¶
Limits the size of a single order by quantity and notional value. Prevents fat-finger errors.
What it controls (by axis):
- Broker axis: global hard cap applied to all orders. Additive - checked in addition to other axes.
- Asset axis: limit per settlement asset.
- Account+Asset axis: limit per account and settlement asset pair.
There is no per-account (without asset) axis.
Override rule for the asset chain: if an account+asset barrier is configured for a given pair, it overrides the asset barrier for that account. If no account+asset barrier exists but an asset barrier does, the asset barrier applies. If neither is configured, the axis does not check the order. The broker axis is always additive regardless of the asset chain outcome.
When both an asset-chain axis and the broker axis reject the same order, the asset-chain reject is returned first.
Limit parameters (OrderSizeLimit):
| Field | Type | Description |
|---|---|---|
max quantity |
Quantity | maximum quantity per order |
max notional |
Volume | maximum notional value per order |
For a volume-based trade amount, notional is taken directly. For a
quantity-based trade amount, notional is derived as |price| * quantity.
price must be provided when this conversion is needed.
Required fields to populate:
Order:instrument,trade amount,account id(when account+asset axes are configured),price(when quantity-to-notional conversion is needed)Execution Report: none
Rejects:
- Quantity exceeded
Code:
OrderQtyExceedsLimitReason:order quantity exceeded - Notional exceeded
Code:
OrderNotionalExceedsLimitReason:order notional exceeded - Both exceeded
Code:
OrderExceedsLimitReason:order size exceeded - Missing
priceor failed size translation Code:OrderValueCalculationFailedReason:order value calculation failed - Missing required field
Code:
MissingRequiredField
Go
usd, err := param.NewAsset("USD")
maxQty, err := param.NewQuantityFromString("100")
maxNotional, err := param.NewVolumeFromString("50000")
engine, err := openpit.NewEngineBuilder().
NoSync().
Builtin(
policies.BuildOrderSizeLimit().
AssetBarriers(
policies.OrderSizeAssetBarrier{
SettlementAsset: usd,
Limit: policies.OrderSizeLimit{
MaxQuantity: maxQty,
MaxNotional: maxNotional,
},
},
).
BrokerBarrier(
policies.OrderSizeBrokerBarrier{
Limit: policies.OrderSizeLimit{
MaxQuantity: maxQty,
MaxNotional: maxNotional,
},
},
),
).
Build()
Python
import openpit
import openpit.pretrade.policies
engine = (
openpit.Engine.builder()
.no_sync()
.builtin(
openpit.pretrade.policies.build_order_size_limit()
.asset_barriers(
openpit.pretrade.policies.OrderSizeAssetBarrier(
limit=openpit.pretrade.policies.OrderSizeLimit(
max_quantity=openpit.param.Quantity(100),
max_notional=openpit.param.Volume(50000),
),
settlement_asset="USD",
),
)
.broker_barrier(
openpit.pretrade.policies.OrderSizeBrokerBarrier(
limit=openpit.pretrade.policies.OrderSizeLimit(
max_quantity=openpit.param.Quantity(10000),
max_notional=openpit.param.Volume(5000000),
),
),
)
)
.build()
)
C++
namespace policies = openpit::pretrade::policies;
const openpit::param::Quantity maxQty = openpit::param::Quantity::FromString("100");
const openpit::param::Volume maxNotional = openpit::param::Volume::FromString("50000");
openpit::EngineBuilder builder(openpit::SyncPolicy::None);
builder.Add(
policies::OrderSizeLimitPolicy{}
.AssetBarrier(policies::OrderSizeAssetBarrier(
policies::OrderSizeLimit(maxQty, maxNotional), "USD"))
.BrokerBarrier(policies::OrderSizeBrokerBarrier(
policies::OrderSizeLimit(maxQty, maxNotional))));
const openpit::Engine engine = builder.Build();
Rust
use openpit::param::{Asset, Quantity, Volume};
use openpit::pretrade::policies::{
OrderSizeAssetBarrier,
OrderSizeLimit,
OrderSizeLimitPolicy,
OrderSizeLimitSettings,
};
use openpit::storage::NoLocking;
use openpit::{
Engine, OrderOperation, WithExecutionReportOperation, WithFinancialImpact,
};
type Report = WithExecutionReportOperation<WithFinancialImpact<()>>;
let engine = Engine::builder::<OrderOperation, Report, ()>()
.no_sync()
.pre_trade(OrderSizeLimitPolicy::<NoLocking>::new(
OrderSizeLimitSettings::new(
None,
[OrderSizeAssetBarrier {
limit: OrderSizeLimit {
max_quantity: Quantity::from_str("100")?,
max_notional: Volume::from_str("50000")?,
},
settlement_asset: Asset::new("USD")?,
}],
[],
)?,
))
.build()?;
PnlBoundsKillSwitchPolicy¶
Tracks accumulated realized P&L and blocks new orders when it moves outside
configured bounds. After a kill switch triggers, the engine blocks the
affected account id from every subsequent pre-trade request across all
settlement assets - see Account Blocking by Engine
for how the block is recorded and cleared.
What it controls (by axis):
- Broker barrier: P&L bounds applied to all accounts with a given settlement asset. If no broker barrier is configured for an order's settlement asset and no account barrier matches, the order passes without P&L tracking.
- Account+Asset barrier: P&L bounds for a specific account and settlement
asset pair. Accepts an
initial pnlto resume tracking from a previously accumulated value.
Both barriers are checked on every order; the broker barrier is checked first.
Barrier parameters (PnlBoundsBrokerBarrier):
| Field | Type | Required | Description |
|---|---|---|---|
settlement asset |
Asset | yes | settlement asset for P&L tracking |
lower bound |
optional P&L | no | loss limit (typically negative) |
upper bound |
optional P&L | no | profit-taking limit (typically positive) |
At least one bound must be set. The constructor does not validate bound signs or
the order of lower bound <= upper bound. Bounds are exclusive: equality
with a configured lower or upper bound is accepted; only values below the lower
bound or above the upper bound breach.
P&L accumulation uses the account id from the execution report (not from the
original order). Each report contributes pnl + fee to the accumulated total.
P&L overflow causes an account block (cleared like any engine-recorded block;
see When a block is cleared).
Post-trade behavior:
- Accepts two valid ways to pass trading result:
- Separate:
pnlis net trading P&L before fees,feeis provided separately. The engine appliesfeeas a negative P&L contribution. - Combined:
pnlalready includes fees,feereturnsNoneor zero. In that case the fee is not counted twice.
Required fields to populate:
Order:instrument,account idExecution Report:instrument,account id,pnl,fee
Rejects:
- First detection via broker barrier
Code:
PnlKillSwitchTriggered, Scope:accountReason:pnl kill switch triggered: broker barrier - First detection via account+asset barrier
Code:
PnlKillSwitchTriggered, Scope:accountReason:pnl kill switch triggered: account+asset barrier - All subsequent checks after kill switch has triggered
Code:
PnlKillSwitchTriggered, Scope:accountReason:pnl kill switch triggered: account blocked - Missing required field
Code:
MissingRequiredField
Go
usd, err := param.NewAsset("USD")
lowerBound, err := param.NewPnlFromString("-1000")
upperBound, err := param.NewPnlFromString("500")
engine, err := openpit.NewEngineBuilder().
NoSync().
Builtin(
policies.BuildPnlBoundsKillswitch().
BrokerBarriers(
policies.PnlBoundsBrokerBarrier{
SettlementAsset: usd,
LowerBound: optional.Some(lowerBound),
UpperBound: optional.Some(upperBound),
},
),
).
Build()
Python
import openpit
import openpit.pretrade.policies
engine = (
openpit.Engine.builder()
.no_sync()
.builtin(
openpit.pretrade.policies.build_pnl_bounds_killswitch()
.broker_barriers(
openpit.pretrade.policies.PnlBoundsBrokerBarrier(
settlement_asset=openpit.param.Asset("USD"),
lower_bound=openpit.param.Pnl(-1000),
upper_bound=openpit.param.Pnl(500),
),
)
)
.build()
)
C++
namespace policies = openpit::pretrade::policies;
policies::PnlBoundsBrokerBarrier barrier("USD");
barrier.lowerBound = openpit::param::Pnl::FromString("-1000");
barrier.upperBound = openpit::param::Pnl::FromString("500");
openpit::EngineBuilder builder(openpit::SyncPolicy::None);
builder.Add(
policies::PnlBoundsKillSwitchPolicy{}.BrokerBarrier(std::move(barrier)));
const openpit::Engine engine = builder.Build();
Rust
use openpit::param::{Asset, Pnl};
use openpit::pretrade::policies::{
PnlBoundsBrokerBarrier,
PnlBoundsKillSwitchPolicy,
PnlBoundsKillSwitchSettings,
};
use openpit::{
Engine, OrderOperation, WithExecutionReportOperation, WithFinancialImpact,
};
type Report = WithExecutionReportOperation<WithFinancialImpact<()>>;
let builder = Engine::builder::<OrderOperation, Report, ()>().no_sync();
let policy = PnlBoundsKillSwitchPolicy::new(
PnlBoundsKillSwitchSettings::new(
[PnlBoundsBrokerBarrier {
settlement_asset: Asset::new("USD")?,
lower_bound: Some(Pnl::from_str("-1000")?),
upper_bound: Some(Pnl::from_str("500")?),
}],
[],
)?,
builder.storage_builder(),
);
let engine = builder.pre_trade(policy).build()?;
Two PnL kill switches. This standalone policy trusts an externally supplied realized-PnL figure and watches it on the settlement-asset axis, with a scalar fee added to the accumulated total and no FX involved.
SpotFundsPolicyoffers a second kill switch that instead computes realized PnL from the fills it already reconciles, on the account-currency axis, with a structured fee and an FX fail-safe. See Spot Funds - Two Ways to Watch PnL to choose between them.
Custom Policy API¶
Custom policy interfaces, callbacks, and language-specific examples: Policy API.
Related Pages¶
- Pre-trade Pipeline: Request and reservation semantics
- Policy API: Custom policy interfaces and examples
- Dynamic Policy Reconfiguration: Retune built-in policies at runtime without rebuilding the engine
- Domain Types: Value types used by built-in and custom policies
- Reject Codes: Standard business reject codes
- Architecture: Public integration model
- Storage: Built-in synchronization-aware key-value storage