Overview
flora uses three storage backends for different purposes:
PostgreSQL - Relational data (deployments, tokens, KV metadata, secrets)
Redis/Valkey - Cache and session storage
Sled - Per-guild key-value data (on-disk embedded database)
PostgreSQL
Connection Management
Using sqlx with async connection pooling.
let db_pool = PgPoolOptions :: new ()
. max_connections ( config . database . max_connections)
. connect ( & config . database . url)
. await ? ;
Source: apps/runtime/src/main.rs:76
Configuration:
Default max connections: 5
Configurable via DATABASE_MAX_CONNECTIONS
Connection pooling handled by sqlx
Migrations
Migrations run automatically at startup using sqlx migrations.
static MIGRATOR : Migrator = sqlx :: migrate! ( "./migrations" );
MIGRATOR . run ( & db_pool ) . await ? ;
Source: apps/runtime/src/main.rs:81
Migration files: apps/runtime/migrations/
Schema
CREATE TABLE deployments (
id BIGSERIAL PRIMARY KEY ,
guild_id TEXT NOT NULL UNIQUE ,
entry_point TEXT NOT NULL ,
bundle TEXT NOT NULL ,
source_map JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW (),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW ()
);
Stores guild script bundles and source maps for error reporting.
CREATE TABLE kv_stores (
id BIGSERIAL PRIMARY KEY ,
guild_id TEXT NOT NULL ,
store_name TEXT NOT NULL ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW (),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW (),
UNIQUE (guild_id, store_name)
);
Indexes KV stores. Actual key-value data lives in Sled.
CREATE TABLE tokens (
id BIGSERIAL PRIMARY KEY ,
token_hash TEXT NOT NULL UNIQUE ,
guild_id TEXT NOT NULL ,
description TEXT ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW (),
last_used_at TIMESTAMPTZ
);
API tokens for CLI authentication.
CREATE TABLE secrets (
id BIGSERIAL PRIMARY KEY ,
guild_id TEXT NOT NULL ,
key TEXT NOT NULL ,
encrypted_value BYTEA NOT NULL ,
placeholder TEXT NOT NULL ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW (),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW (),
UNIQUE (guild_id, key )
);
Encrypted guild secrets (encrypted with ChaCha20-Poly1305).
Query Patterns
Compile-time checked queries:
let row = sqlx :: query_as :: < _ , KvStoreRow >(
r#"
SELECT id, guild_id, store_name, created_at, updated_at
FROM kv_stores
WHERE guild_id = $1 AND store_name = $2
"# ,
)
. bind ( guild_id )
. bind ( store_name )
. fetch_one ( & self . db)
. await ? ;
Source: apps/runtime/src/services/kv/service.rs:92
Transaction support:
let mut tx = db . begin () . await ? ;
sqlx :: query ( "INSERT INTO ..." )
. execute ( & mut * tx )
. await ? ;
tx . commit () . await ? ;
Redis/Valkey
Connection Management
Using fred client with connection pooling and automatic reconnection.
let cache_client = Builder :: from_config ( cache_config )
. with_connection_config ( | config | {
config . connection_timeout = Duration :: from_secs ( 10 );
})
. set_policy ( ReconnectPolicy :: new_exponential ( 0 , 100 , 30_000 , 2 ))
. build_pool ( config . cache . pool_size)
. expect ( "Failed to create redis pool" );
Source: apps/runtime/src/main.rs:86
Reconnection policy:
Exponential backoff
Start: 100ms
Max: 30s
Multiplier: 2x
Use Cases
Session Storage
OAuth sessions stored with TTL.
pub async fn create_session (
& self ,
user_id : & str ,
guild_id : & str ,
) -> Result < String > {
let session_id = generate_session_id ();
let key = format! ( "session:{}" , session_id );
let value = serde_json :: json! ({
"user_id" : user_id ,
"guild_id" : guild_id ,
});
self . redis
. set ( & key , value . to_string (), Some ( Expiration :: EX ( self . ttl)), None , false )
. await ? ;
Ok ( session_id )
}
Source: apps/runtime/src/services/auth/service.rs
Deployment Cache
Deployments cached to reduce PostgreSQL load.
let cache_key = format! ( "deployment:{}" , guild_id );
if let Ok ( cached ) = self . cache . get :: < String , String >( & cache_key ) . await {
return Ok ( serde_json :: from_str ( & cached ) ? );
}
Source: apps/runtime/src/services/deployments/mod.rs
Cache invalidation:
On deployment upsert
On deployment delete
TTL-based expiration
Data Structures
Strings:
Session data (JSON serialized)
Cached deployments
Sets:
Active guild IDs
User permissions
Hashes:
User profiles
Guild metadata
Sled
Architecture
Sled is an embedded database (like SQLite) optimized for concurrent workloads.
Source: apps/runtime/src/services/kv/service.rs:24
Key features:
Lock-free reads
ACID transactions
Zero-copy operations
Crash-safe (write-ahead log)
File Layout
data/kv/
├── {guild_id}/
│ ├── {store_name}/
│ │ ├── conf
│ │ ├── db
│ │ └── snap.{timestamp}/
│ └── backups/
│ └── {backup_id}/
│ └── {store_name}/
Each guild-store pair gets its own Sled instance.
Instance Caching
Sled instances are cached in memory to avoid reopening.
pub struct KvService {
db : Pool < Postgres >,
db_cache : Arc < RwLock < BoundedCache >>,
base_path : PathBuf ,
}
Source: apps/runtime/src/services/kv/service.rs:24
Cache policy:
LRU eviction
Max size: configurable (default 100 instances)
Thread-safe via Arc<RwLock>
Source: apps/runtime/src/services/kv/cache.rs
KV Operations
Set
pub async fn set (
& self ,
guild_id : & str ,
store_name : & str ,
key : & str ,
value : & str ,
expiration : Option < i64 >,
metadata : Option < serde_json :: Value >,
) -> Result <()> {
let db = self . get_or_open_db ( guild_id , store_name ) ? ;
db . insert ( key . as_bytes (), value . as_bytes ()) ? ;
if expiration . is_some () || metadata . is_some () {
let key_metadata = RawKvKeyMetadata { expiration , metadata };
let metadata_bytes = serde_json :: to_vec ( & key_metadata ) ? ;
self . get_metadata_tree ( & db ) ?
. insert ( key . as_bytes (), metadata_bytes ) ? ;
}
Ok (())
}
Source: apps/runtime/src/services/kv/service.rs:140
Get
pub async fn get (
& self ,
guild_id : & str ,
store_name : & str ,
key : & str ,
) -> Result < Option < String >> {
let db = self . get_or_open_db ( guild_id , store_name ) ? ;
match db . get ( key . as_bytes ()) ? {
Some ( bytes ) => Ok ( Some ( String :: from_utf8 ( bytes . to_vec ()) ? )),
None => Ok ( None ),
}
}
Source: apps/runtime/src/services/kv/service.rs:107
List
pub async fn list_keys (
& self ,
guild_id : & str ,
store_name : & str ,
prefix : Option < & str >,
limit : Option < u32 >,
cursor : Option < & str >,
) -> Result < RawKvListKeysResult > {
let db = self . get_or_open_db ( guild_id , store_name ) ? ;
let start_key = cursor . or ( prefix ) . unwrap_or ( "" );
let iter = db . range ( start_key . as_bytes () .. );
// ... pagination logic
}
Source: apps/runtime/src/services/kv/service.rs:214
Pagination:
Cursor-based (key-based continuation)
Default limit: 100
Max limit: 1000
Each Sled instance has a separate tree for metadata.
const METADATA_TREE_NAME : & str = "__metadata" ;
fn get_metadata_tree ( & self , db : & Db ) -> Result < sled :: Tree > {
Ok ( db . open_tree ( METADATA_TREE_NAME ) ? )
}
Source: apps/runtime/src/services/kv/service.rs:346
Metadata structure:
pub struct RawKvKeyMetadata {
pub expiration : Option < i64 >, // Unix timestamp
pub metadata : Option < serde_json :: Value >,
}
Stored separately to avoid inflating all key reads.
Size Limits
Limit Default Config Max value size 1 MB MAX_VALUE_SIZEMax key length 512 chars - Max store name 64 chars - List limit 100 DEFAULT_LIST_LIMITList max 1000 MAX_LIST_LIMIT
Source: apps/runtime/src/services/kv/validation.rs
Backup and Export
pub async fn export_guild ( & self , guild_id : & str ) -> Result < String > {
let stores = self . list_stores ( guild_id ) . await ? ;
let backup_id = Utc :: now () . timestamp () . to_string ();
let backup_dir = self . base_path
. join ( guild_id )
. join ( "backups" )
. join ( & backup_id );
for store in stores {
let db = self . get_or_open_db ( guild_id , & store . store_name) ? ;
db . flush () ? ; // Ensure all writes are persisted
copy_dir_all ( & db_path , & store_backup_dir ) ? ;
}
Ok ( backup_id )
}
Source: apps/runtime/src/services/kv/service.rs:293
Backup strategy:
Flush all pending writes
Copy Sled directory to backup location
Return backup ID (timestamp)
Storage Comparison
Feature PostgreSQL Redis Sled Type Relational Key-Value Embedded KV Persistence Durable Optional Durable Transactions Full ACID Limited ACID Queries SQL Commands Range scans Network TCP TCP In-process Use Case Structured data Cache/sessions Guild KV Backup pg_dump RDB/AOF Directory copy
Data Flow Example
Setting a KV value
HTTP API receives request
Verify store exists in PostgreSQL
Open Sled instance (or get from cache)
Insert key-value into Sled
Insert metadata into Sled metadata tree (if provided)
Flush to disk (async)
Loading a deployment
Check Redis cache for deployment
If miss, query PostgreSQL
Cache in Redis with TTL
Return deployment to caller
Storing a secret
Encrypt value with ChaCha20-Poly1305
Derive placeholder from key
Insert into PostgreSQL secrets table
Refresh runtime secrets for guild
PostgreSQL
[ database ]
max_connections = 10 # Increase for high load
Considerations:
More connections = more memory
Monitor connection pool usage
Use read replicas for scaling
Redis
[ cache ]
pool_size = 20 # Increase for high concurrency
Considerations:
Connection pooling reduces latency
Monitor cache hit rate
Set appropriate TTLs
Sled
Configuration:
Sled auto-tunes for workload
Increase cache size via BoundedCache
Flush periodically to reduce memory
const MAX_DB_CACHE_SIZE : usize = 100 ;
Source: apps/runtime/src/services/kv/cache.rs
Sled instances are kept open in cache. Limit cache size to prevent memory exhaustion.