Storage Backends¶
Tryx supports a 3-tier storage architecture so you can pick the right balance between simplicity, performance, and flexibility for your project.
| Tier | Backend | Language | Overhead | When to use |
|---|---|---|---|---|
| 1 | SqliteStore |
Built-in (Rust) | Zero | Default / prototyping / single-instance |
| 2 | FFI Store | Native (C ABI) | Near-zero | Maximum throughput (Postgres, custom C/Rust) |
| 3 | StoreBase |
Pure Python | Low | Rapid development / exotic backends (Redis, Mongo) |
flowchart LR
subgraph Tryx["Tryx Rust Core"]
Backend["Backend trait\n(SignalStore + AppSyncStore +\nProtocolStore + DeviceStore +\nMsgSecretStore)"]
end
SQLite["SqliteStore\n(built-in)"]
FFI["FfiBridgeStore\n(C ABI .so/.dll)"]
Python["PythonStore\n(async Python)"]
Backend --> SQLite
Backend --> FFI
Backend --> Python
style SQLite fill:#22c55e,color:#fff
style FFI fill:#3b82f6,color:#fff
style Python fill:#a855f7,color:#fff
Tier 1: SqliteStore (Default)¶
The built-in SQLite backend requires zero configuration. Just pass a file path:
from tryx.backend import SqliteStore
from tryx.client import Tryx
app = Tryx(SqliteStore("whatsapp.db"))
Internally, the Rust core opens the database with WAL mode, creates all tables automatically, and handles migrations. This is the recommended backend for development and single-instance production deployments.
Tier 2: FFI Store (Native Shared Library)¶
For maximum throughput with no Python overhead, implement a storage backend as a native shared library (.so / .dylib / .dll) that exports C-ABI entry points.
Architecture¶
sequenceDiagram
participant Rust as Tryx Rust Core
participant C as Native Store (.so)
participant DB as Database (Postgres/etc)
Rust->>C: tryx_store_connect(dsn)
C->>DB: Open connection pool
C-->>Rust: handle ptr
loop Every operation
Rust->>C: tryx_put_identity(handle, addr, key, len)
C->>DB: INSERT INTO identities ...
C-->>Rust: status code (0 = OK)
end
Usage¶
Any object with lib_path and config_json attributes is detected as an FFI backend:
import json
from tryx.client import Tryx
# tryx-store-postgres exposes this interface
class PostgresStore:
lib_path = "./libtryx_pg.so"
config_json = json.dumps({"host": "localhost", "dbname": "tryx"})
app = Tryx(PostgresStore())
Type Safety¶
Use the FfiStoreProtocol for type checking:
from tryx.backend import FfiStoreProtocol
def create_backend() -> FfiStoreProtocol:
return PostgresStore() # type checker validates attributes
Required C ABI Entry Points¶
Your shared library must export these symbols:
| Symbol | Signature |
|---|---|
tryx_store_connect |
(dsn: *const c_char, handle: *mut *mut c_void) -> i32 |
tryx_store_destroy |
(handle: *mut c_void) |
tryx_put_identity |
(handle, addr, key_ptr, key_len) -> i32 |
tryx_load_identity |
(handle, addr, out: *mut TryxBuffer) -> i32 |
tryx_delete_identity |
(handle, addr) -> i32 |
| ... | (see ffi_bridge.rs for the full list of ~30 entry points) |
All functions return 0 on success, non-zero on error.
Tier 3: PythonStore (Pure Python)¶
For maximum flexibility, inherit from StoreBase and implement all abstract methods using any async Python library.
Architecture¶
sequenceDiagram
participant Rust as Tryx Rust Core (Tokio)
participant Bridge as PythonStore Bridge
participant GIL as Python GIL
participant Py as Your StoreBase subclass
Rust->>Bridge: put_identity("addr", key_bytes)
Bridge->>GIL: Python::attach(|py| ...)
GIL->>Py: await store.put_identity(address="addr", key=b"...")
Py-->>GIL: None
GIL-->>Bridge: PyObject
Bridge-->>Rust: Ok(())
Note over GIL: GIL is released during<br/>the Python await
Usage¶
import json
import redis.asyncio as redis
from tryx.backend import StoreBase
from tryx.client import Tryx
class RedisStore(StoreBase):
def __init__(self, url: str = "redis://localhost"):
self.r = redis.from_url(url)
async def put_identity(self, address: str, key: bytes) -> None:
await self.r.set(f"identity:{address}", key)
async def load_identity(self, address: str) -> bytes | None:
return await self.r.get(f"identity:{address}")
async def delete_identity(self, address: str) -> None:
await self.r.delete(f"identity:{address}")
# ... implement ALL abstract methods from StoreBase ...
app = Tryx(RedisStore())
Type Checking¶
StoreBase is an ABC with @abstractmethod on every required method. If you miss any method:
- Mypy/Pyright will report an error at class definition
- Runtime will raise
TypeError: Can't instantiate abstract class
class IncompleteStore(StoreBase):
async def put_identity(self, address: str, key: bytes) -> None:
pass
# Missing 50+ methods → type checker error!
Performance Characteristics¶
The PythonStore bridge is designed for low overhead:
| Aspect | Detail |
|---|---|
| GIL acquisition | Uses Python::attach() (PyO3 0.28+), the lightest available mechanism |
| Async bridging | pyo3_async_runtimes::tokio::into_future() — zero-copy future conversion |
| Argument passing | Scalars (str, int, bool) passed natively via kwargs; complex structs as JSON bytes |
| GIL during I/O | Released during Python await — other Rust tasks run freely |
| Cloning | Py<T>::clone_ref() — reference count increment only, no deep copy |
Typical overhead per call: ~2-5µs for GIL acquire/release + Python method dispatch. The actual database I/O dominates.
API Specification¶
All three tiers implement the same underlying Rust traits. Here is the complete method reference grouped by trait:
SignalStore — End-to-End Encryption¶
Handles identity keys, sessions, pre-keys, signed pre-keys, and sender keys.
| Method | Args | Returns | Description |
|---|---|---|---|
put_identity |
address: str, key: bytes |
None |
Store 32-byte identity key |
load_identity |
address: str |
bytes \| None |
Load identity key (32 bytes) |
delete_identity |
address: str |
None |
Delete identity key |
get_session |
address: str |
bytes \| None |
Get encrypted session record |
put_session |
address: str, session: bytes |
None |
Store session record |
delete_session |
address: str |
None |
Delete session |
store_prekey |
id: int, record: bytes, uploaded: bool |
None |
Store pre-key |
load_prekey |
id: int |
bytes \| None |
Load pre-key |
remove_prekey |
id: int |
None |
Remove pre-key |
get_max_prekey_id |
— | int |
Max stored pre-key ID (or 0) |
store_signed_prekey |
id: int, record: bytes |
None |
Store signed pre-key |
load_signed_prekey |
id: int |
bytes \| None |
Load signed pre-key |
load_all_signed_prekeys |
— | list[tuple[int, bytes]] |
All signed pre-keys |
remove_signed_prekey |
id: int |
None |
Remove signed pre-key |
put_sender_key |
address: str, record: bytes |
None |
Store group sender key |
get_sender_key |
address: str |
bytes \| None |
Get group sender key |
delete_sender_key |
address: str |
None |
Delete group sender key |
AppSyncStore — App State Synchronization¶
| Method | Args | Returns | Description |
|---|---|---|---|
get_sync_key |
key_id: bytes |
bytes \| None |
Get sync key (JSON AppStateSyncKey) |
set_sync_key |
key_id: bytes, key: bytes |
None |
Set sync key |
get_version |
name: str |
bytes |
Get collection version (JSON HashState) |
set_version |
name: str, state: bytes |
None |
Set collection version |
put_mutation_macs |
name: str, version: int, mutations: bytes |
None |
Store mutation MACs |
get_mutation_mac |
name: str, index_mac: bytes |
bytes \| None |
Get mutation MAC |
delete_mutation_macs |
name: str, index_macs: bytes |
None |
Delete mutation MACs |
get_latest_sync_key_id |
— | bytes \| None |
Latest sync key ID |
DeviceStore — Device Persistence¶
| Method | Args | Returns | Description |
|---|---|---|---|
save |
device: bytes |
None |
Save device data (JSON Device) |
load |
— | bytes \| None |
Load device data |
exists |
— | bool |
Check if device exists |
create |
— | int |
Create device, return ID |
ProtocolStore — Protocol Alignment¶
| Method | Args | Returns | Description |
|---|---|---|---|
get_sender_key_devices |
group_jid: str |
list[tuple[str, bool]] |
SKDM device status |
set_sender_key_status |
group_jid: str, entries: bytes |
None |
Set SKDM status |
clear_sender_key_devices |
group_jid: str |
None |
Clear group SKDM tracking |
delete_sender_key_device_rows |
device_jids: bytes |
None |
Delete by device JID |
clear_all_sender_key_devices |
— | None |
Clear all SKDM tracking |
get_lid_mapping |
lid: str |
bytes \| None |
LID→PN mapping (JSON) |
get_pn_mapping |
phone: str |
bytes \| None |
PN→LID mapping (JSON) |
put_lid_mapping |
entry: bytes |
None |
Upsert LID-PN mapping |
get_all_lid_mappings |
— | list[bytes] |
All mappings (JSON array) |
save_base_key |
address: str, message_id: str, base_key: bytes |
None |
Retry collision detection |
has_same_base_key |
address: str, message_id: str, current_base_key: bytes |
bool |
Compare base keys |
delete_base_key |
address: str, message_id: str |
None |
Delete base key |
update_device_list |
record: bytes |
None |
Update device registry (JSON) |
get_devices |
user: str |
bytes \| None |
Get device list (JSON) |
delete_devices |
user: str |
None |
Delete device list |
get_tc_token |
jid: str |
bytes \| None |
Get trust token (JSON) |
put_tc_token |
jid: str, entry: bytes |
None |
Set trust token |
delete_tc_token |
jid: str |
None |
Delete trust token |
get_all_tc_token_jids |
— | list[str] |
All JIDs with tokens |
delete_expired_tc_tokens |
cutoff: int |
int |
Prune old tokens |
store_sent_message |
chat_jid: str, message_id: str, payload: bytes |
None |
Store for retry |
take_sent_message |
chat_jid: str, message_id: str |
bytes \| None |
Atomic take |
delete_expired_sent_messages |
cutoff: int |
int |
Prune old messages |
MsgSecretStore — Message Secret Persistence¶
| Method | Args | Returns | Description |
|---|---|---|---|
put_msg_secrets |
entries: bytes |
int |
Batch upsert (JSON [MsgSecretEntry]) |
get_msg_secret |
chat: str, sender: str, msg_id: str |
bytes \| None |
Fetch secret |
delete_expired_msg_secrets |
cutoff: int |
int |
Prune expired secrets |
JSON Struct Schemas¶
Complex types are passed as JSON bytes. Here are the key schemas:
AppStateSyncKey¶
LidPnMappingEntry¶
{
"lid": "100000012345678",
"phone_number": "559980000001",
"created_at": 1717430400,
"updated_at": 1717430400,
"learning_source": "usync"
}
TcTokenEntry¶
MsgSecretEntry¶
{
"chat": "5599800001@s.whatsapp.net",
"sender": "5599800002@s.whatsapp.net",
"msg_id": "3EB0A1B2C3D4E5F6",
"secret": [/* 32 u8 bytes */],
"expires_at": 0,
"message_ts": 1717430400
}
DeviceListRecord¶
{
"user": "559980000001",
"devices": [
{"device_id": 0, "key_index": null},
{"device_id": 1, "key_index": 42}
],
"timestamp": 1717430400,
"phash": "abc123",
"raw_id": 5
}
Choosing a Backend¶
| Criteria | SqliteStore | FFI Store | PythonStore |
|---|---|---|---|
| Setup complexity | ⭐ Zero | ⭐⭐⭐ Requires compilation | ⭐⭐ Moderate |
| Throughput | ⭐⭐ Good | ⭐⭐⭐ Maximum | ⭐⭐ Good |
| Multi-instance | ❌ Single file | ✅ Shared DB | ✅ Shared DB |
| Language | Rust (built-in) | C/Rust | Python |
| Development speed | ⭐⭐⭐ Instant | ⭐ Slow | ⭐⭐⭐ Fast |
| Package decoupling | Built-in | ✅ Fully independent | ✅ Fully independent |