Architecture
How Tally UI's connector and trait system works
The Problem
Every POS backend shapes its product data differently. Look at how WooCommerce and Medusa represent the same espresso machine:
WooCommerce calls it name, Medusa calls it title. WooCommerce puts price as a string on the product, Medusa stores it as an integer (in cents) nested inside variants. Images use src in one and url in the other.
Forcing all this into one universal schema would lose data. Leaving it raw would mean every component needs if (connector === 'woocommerce') branches everywhere. Neither works.
The Trait Layer
Tally UI solves this with a trait layer — a thin set of accessor functions that each connector implements. Components program against traits, not raw data.
UI Component → calls getName(doc)
↓
Trait Layer → WooCommerce: doc.name
Medusa: doc.title
Vendure: doc.name
↓
RxDB Document → stored in connector-specific schemaThe active connector determines which implementation runs. Components never import connector-specific code — they use the useProductTraits() hook which reads from the nearest ConnectorProvider.
// This component works identically with WooCommerce, Medusa, or any connector
function ProductCard({ doc }) {
const { getName, getPrice, getImageUrl } = useProductTraits();
return (
<View className="bg-surface rounded-lg p-4 gap-2">
<Image source={{ uri: getImageUrl(doc) }} className="rounded w-16 h-16" />
<Text className="text-base font-semibold text-foreground">{getName(doc)}</Text>
<Text className="text-sm text-price">${getPrice(doc)}</Text>
</View>
);
}Connectors
A connector is a package that knows how to talk to a specific backend. It implements the TallyConnector interface:
interface TallyConnector {
id: string; // 'woocommerce', 'medusa', etc.
name: string; // Human-readable name
auth: ConnectorAuth; // How to authenticate
schemas: { // RxDB schemas (per-connector shape)
products: RxJsonSchema;
};
traits: { // Accessor functions
product: ProductTraits;
};
replication: { // Bidirectional sync via RxDB replication
products: ReplicationAdapter;
};
}Per-Connector Schemas
Each connector stores data in its own RxDB schema — faithful to the source API. No data is lost or flattened. The WooCommerce schema has meta_data, attributes, and variations arrays. The Medusa schema has nested variants with prices and options. Both are preserved exactly as the API returns them.
When you need connector-specific fields that aren't covered by traits, you can always access the raw document directly.
Data Flow
┌─────────────────────────────────────────┐
│ Backend API │
│ WooCommerce REST / Medusa Admin / ... │
└──────────────────┬──────────────────────┘
│ replication (pull/push)
┌──────────────────▼──────────────────────┐
│ ReplicationAdapter │
│ Checkpoint-based sync per connector │
│ Server-wins conflict resolution │
└──────────────────┬──────────────────────┘
│ replicateRxCollection
┌──────────────────▼──────────────────────┐
│ RxDB (Local Database) │
│ Connector-specific schemas │
│ IndexedDB (web) / SQLite (mobile) │
└──────────────────┬──────────────────────┘
│ documents
┌──────────────────▼──────────────────────┐
│ Trait Layer │
│ getName() getPrice() getSku() ... │
└──────────────────┬──────────────────────┘
│ hooks
┌──────────────────▼──────────────────────┐
│ UI Components │
│ <ProductTitle> <ProductPrice> ... │
│ Same component, any backend │
└─────────────────────────────────────────┘Replication
Tally UI uses RxDB's built-in replication protocol for bidirectional sync between the local database and remote APIs. Each connector implements a ReplicationAdapter that provides checkpoint-based pull and push handlers.
The replication layer handles:
- Checkpoint tracking -- only fetches data that changed since the last sync
- Bidirectional sync -- pulls remote changes and pushes local edits back
- Conflict resolution -- server-wins strategy (the backend is the source of truth)
- Live updates -- real-time streams where backends support them, polling fallback elsewhere
- Error recovery -- automatic retries with configurable intervals
Platform Storage
Storage is platform-aware. The getStorage() factory returns the right adapter automatically:
| Platform | Storage | Package |
|---|---|---|
| Web browser | IndexedDB (via Dexie) | Built into @tallyui/database |
| React Native | SQLite (via expo-sqlite) | @tallyui/storage-sqlite |
| Node / SSR / tests | In-memory | Built into @tallyui/database |
See the Replication guide for setup and usage details.
Packages
| Package | Purpose |
|---|---|
@tallyui/core | Connector interfaces, trait types, React context |
@tallyui/database | RxDB factory, replication orchestrator, platform storage |
@tallyui/storage-sqlite | RxDB storage adapter for React Native (expo-sqlite) |
@tallyui/components | UI primitives (ProductTitle, ProductPrice, etc.) |
@tallyui/pos | POS business logic — tax, currency, orders, receipts, logging |
@tallyui/theme | Shared design tokens, theming, and cn() utility |
@tallyui/connector-woocommerce | WooCommerce connector |
@tallyui/connector-medusa | MedusaJS v2 connector |
@tallyui/connector-shopify | Shopify Admin API connector |
@tallyui/connector-vendure | Vendure GraphQL connector |