Tally UI

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:

{
  "id": 42,
  "name": "Espresso Machine Pro",
  "slug": "espresso-machine-pro",
  "type": "simple",
  "price": "599.99",
  "regular_price": "599.99",
  "sale_price": "",
  "on_sale": false,
  "sku": "ESP-001",
  "stock_status": "instock",
  "stock_quantity": 15,
  "barcode": "1234567890123",
  "images": [
    {
      "id": 1,
      "src": "https://...",
      "alt": ""
    }
  ],
  "categories": [
    {
      "id": 1,
      "name": "Equipment",
      "slug": "equipment"
    }
  ]
}

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 schema

The 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:

PlatformStoragePackage
Web browserIndexedDB (via Dexie)Built into @tallyui/database
React NativeSQLite (via expo-sqlite)@tallyui/storage-sqlite
Node / SSR / testsIn-memoryBuilt into @tallyui/database

See the Replication guide for setup and usage details.

Packages

PackagePurpose
@tallyui/coreConnector interfaces, trait types, React context
@tallyui/databaseRxDB factory, replication orchestrator, platform storage
@tallyui/storage-sqliteRxDB storage adapter for React Native (expo-sqlite)
@tallyui/componentsUI primitives (ProductTitle, ProductPrice, etc.)
@tallyui/posPOS business logic — tax, currency, orders, receipts, logging
@tallyui/themeShared design tokens, theming, and cn() utility
@tallyui/connector-woocommerceWooCommerce connector
@tallyui/connector-medusaMedusaJS v2 connector
@tallyui/connector-shopifyShopify Admin API connector
@tallyui/connector-vendureVendure GraphQL connector

On this page