Skip to content

Repository files navigation

πŸŒ‰ LegacyBridge

A zero-footprint C++17 ETL pipeline that streams real-time data from legacy Point-of-Sale systems directly into modern Cloud BI dashboards.

C++17 CMake nlohmann/json cpp-httplib OpenSSL Supabase Metabase


πŸ“‹ Table of Contents


πŸ”΄ The Problem

Physical retail stores β€” including restaurants and food & beverage venues β€” depend on legacy Point-of-Sale hardware that locks transactional data inside local CSV log files. This creates a hard wall between real-time floor operations and modern business intelligence.

The obvious fix β€” replacing the hardware β€” is prohibitively expensive and operationally disruptive. Store owners are left blind: no live sales dashboards, no real-time per-table or per-category signals, no cross-venue analytics.


βœ… The Solution

LegacyBridge is a lightweight C++ agent that runs silently alongside existing POS infrastructure. It:

  1. Monitors a local CSV log (mock_pos.csv) for newly appended order rows, polling every 2 seconds
  2. Parses each new row β€” Timestamp, Table_ID, Item_Name, Category, Price β€” into a structured OrderInfo record
  3. Queues records into a thread-safe buffer shared between the file watcher and the cloud uploader
  4. Streams them securely over HTTPS to Supabase's REST API (/rest/v1/orders)
  5. Retries automatically on network failure, ensuring no order is silently dropped

The result: live sales data flows into Metabase BI dashboards in near-real-time β€” with no hardware replacement and no operational downtime.


πŸ—οΈ Architecture Deep Dive

LegacyBridge is built around a decoupled Producer-Consumer architecture. POSWatcher and CloudUploader run on independent threads, communicating exclusively through a shared Buffer β€” isolating file I/O from network I/O.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          LegacyBridge Agent                         β”‚
β”‚                                                                     β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    Buffer (utils.h)     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚   POSWatcher     β”‚ ──── std::mutex ───────▢│  CloudUploader  β”‚  β”‚
β”‚   β”‚   (Producer)     β”‚   std::condition_var    β”‚   (Consumer)    β”‚  β”‚
β”‚   β”‚                  β”‚                         β”‚                 β”‚  β”‚
β”‚   β”‚  seekg / tellg   β”‚      OrderInfo          β”‚  cpp-httplib    β”‚  β”‚
β”‚   β”‚  2s poll cycle   β”‚      queue<T>           β”‚  Retry (5x/3s)  β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                         β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚            β”‚                                          β”‚             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚                                          β”‚
             β–Ό                                          β–Ό
      [ mock_pos.csv ]                    [ Supabase REST API ]
      Timestamp, Table_ID,                /rest/v1/orders
      Item_Name, Category, Price         (PostgreSQL + Pooling)
                                                        β”‚
                                                        β–Ό
                                             [ Metabase Dashboard ]
                                              (Live BI Analytics)

Thread & Data Flow

Stage Component Responsibility
Watch POSWatcher Tails mock_pos.csv for new rows via seekg/tellg; skips header
Parse POSWatcher::parseLine Tokenizes CSV row into an OrderInfo struct
Buffer Buffer (utils.h/.cpp) Thread-safe queue using std::mutex + std::condition_variable
Upload CloudUploader::sendToCloudWithRetry Serializes to JSON; POSTs to Supabase with up to 5 retries
Persist Supabase / PostgreSQL Stores rows in the orders table via REST API
Visualize Metabase Renders live dashboards connected to the Supabase database

πŸ› οΈ Tech Stack

Layer Technology Detail
Core Engine C++17 std::thread, std::mutex, std::condition_variable
Build System CMake 3.10+ with FetchContent Auto-fetches all C++ dependencies at configure time
JSON nlohmann/json v3.11.3 Serializes OrderInfo to JSON payload
HTTP Client cpp-httplib v0.15.3 HTTPS POST to Supabase REST API
TLS/SSL OpenSSL (CPPHTTPLIB_OPENSSL_SUPPORT) Secure transport; required system dependency
Cloud DB Supabase (PostgreSQL) REST API target at /rest/v1/orders
Analytics Metabase BI dashboards connected to the Supabase database
Simulation Python 3 Mock POS log generator (mock_pos.py)

πŸ“ Project Structure

LegacyBridge/
β”œβ”€β”€ CMakeLists.txt        # Build config: FetchContent for json & httplib, links OpenSSL
β”œβ”€β”€ main.cpp              # Entry point: constructs Buffer, POSWatcher, CloudUploader; spawns threads
β”œβ”€β”€ POSWatchdog.h         # POSWatcher class declaration (file path, buffer ref, last_pos)
β”œβ”€β”€ POSWatchdog.cpp       # Implements 2s poll loop (seekg/tellg) and parseLine()
β”œβ”€β”€ CloudUploader.h       # CloudUploader class declaration (buffer ref, Supabase URL & key)
β”œβ”€β”€ CloudUploader.cpp     # Implements start() consumer loop and sendToCloudWithRetry()
β”œβ”€β”€ utils.h               # OrderInfo struct + Buffer class declaration
β”œβ”€β”€ utils.cpp             # Buffer::push() and Buffer::pop() implementations
└── mock_pos.py           # Simulates a live restaurant POS: writes CSV rows every 1–4 seconds

Key Data Structures (utils.h)

// Represents a single parsed POS transaction
struct OrderInfo {
    string timestamp;   // e.g. "2025-01-15 14:32:07"
    int    table_id;    // 1–15
    string item_name;   // e.g. "Shakshuka"
    string category;    // "Food", "Alcohol", or "Beverage"
    float  price;       // In ILS (e.g. 48.0)
};

// Thread-safe queue shared between POSWatcher (producer) and CloudUploader (consumer)
class Buffer {
    queue<OrderInfo>    q;
    mutex               mtx;
    condition_variable  cv;
public:
    void push(OrderInfo info);  // Locks, pushes, notifies consumer
    bool pop(OrderInfo& info);  // Blocks until an item is available
};

πŸš€ Getting Started

Prerequisites

  • A C++17-compliant compiler (g++ 8+ or clang++ 7+)
  • CMake 3.10+
  • OpenSSL development libraries
  • Python 3 (for the mock POS simulator)

Install OpenSSL (if needed):

# Ubuntu / Debian
sudo apt-get install libssl-dev

# macOS (Homebrew)
brew install openssl

Build

All C++ dependencies (nlohmann/json v3.11.3, cpp-httplib v0.15.3) are resolved automatically via CMake's FetchContent during the configure step β€” no manual installs required.

# 1. Clone the repository
git clone https://github.com/your-username/LegacyBridge.git
cd LegacyBridge

# 2. Create an isolated build directory
mkdir build && cd build

# 3. Configure β€” dependencies are fetched automatically here
cmake ..

# 4. Compile
cmake --build .

The LegacyBridge executable will be available inside build/ on success.


βš™οΈ Configuration

Before running, open main.cpp and update the two required values:

1. CSV file path β€” must point to the exact location where mock_pos.py writes its output on your machine:

// main.cpp
POSWatcher watcher("/absolute/path/to/your/mock_pos.csv", order_buffer);

2. Supabase credentials β€” your project's domain and API key:

// main.cpp
string supabase_domain = "your-project-ref.supabase.co";
string supabase_key    = "your-supabase-anon-or-service-key";

Your credentials can be found in the Supabase dashboard under: Project Settings β†’ API β†’ Project URL & API Keys

πŸ”’ Security Warning: Credentials are currently hardcoded as string literals. Do not commit live keys to source control. For any shared or production deployment, load secrets from environment variables or a secrets manager instead.

Supabase Table Schema

The uploader POSTs to /rest/v1/orders. Ensure this table exists in your Supabase project before running:

CREATE TABLE orders (
    id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    timestamp  TEXT,
    table_id   INTEGER,
    item_name  TEXT,
    category   TEXT,
    price      REAL
);

▢️ Running LegacyBridge

LegacyBridge requires two terminals running simultaneously.

Terminal 1 β€” Start the Mock POS Simulator

mock_pos.py simulates a restaurant POS by appending a randomized order row to mock_pos.csv every 1–4 seconds. The menu includes food items (Steak Tartare, Bibimbap Bulgogi, Beef Stir-fry, Shakshuka, Hummus Plate), alcoholic drinks (Goldstar Beer, Arak), and beverages (Cola) β€” all priced in ILS, across 15 tables.

# From the repository root
python3 mock_pos.py

Expected output:

Started writing mock POS data to mock_pos.csv...
Press Ctrl+C to stop.

[2025-01-15 14:32:07] Added order: Table 4 ordered Shakshuka (48 ILS)
[2025-01-15 14:32:10] Added order: Table 11 ordered Goldstar Beer (28 ILS)
[2025-01-15 14:32:13] Added order: Table 2 ordered Bibimbap Bulgogi (72 ILS)

Terminal 2 β€” Start the LegacyBridge Agent

# From the build directory
./LegacyBridge

Expected output (normal operation):

Starting LegacyBridge Service...
Starting POS Watcher on: /path/to/mock_pos.csv
[Uploader] Started. Waiting for data...
[Watcher] Queued new order: Shakshuka (Table 4)
[Uploader] SUCCESS: Sent Shakshuka to cloud.
[Watcher] Queued new order: Goldstar Beer (Table 11)
[Uploader] SUCCESS: Sent Goldstar Beer to cloud.

Expected output (on network failure):

[Uploader] ERROR 0. Attempt 1/5 failed. Retrying in 3s...
[Uploader] ERROR 0. Attempt 2/5 failed. Retrying in 3s...
[Uploader] SUCCESS: Sent Shakshuka to cloud.

Once records are flowing into Supabase, open your Metabase dashboard to see live sales data populate in real time.


πŸ”„ How It Works End-to-End

mock_pos.py                  POSWatcher                  CloudUploader         Supabase
     β”‚                            β”‚                             β”‚                  β”‚
     │─── appends CSV row ───────▢│                             β”‚                  β”‚
     β”‚                            │── seekg(last_pos)           β”‚                  β”‚
     β”‚                            │── getline() delta read      β”‚                  β”‚
     β”‚                            │── skip "Timestamp" header   β”‚                  β”‚
     β”‚                            │── parseLine() β†’ OrderInfo   β”‚                  β”‚
     β”‚                            │── Buffer::push()            β”‚                  β”‚
     β”‚                            β”‚   lock_guard(mtx)           β”‚                  β”‚
     β”‚                            β”‚   q.push(info)              β”‚                  β”‚
     β”‚                            β”‚   cv.notify_one() ─────────▢│                  β”‚
     β”‚                            │── last_pos = tellg()        β”‚                  β”‚
     β”‚                            │── sleep(2s)                 β”‚                  β”‚
     β”‚                            β”‚                             │── cv.wait(lock)  β”‚
     β”‚                            β”‚                             │── q.pop()        β”‚
     β”‚                            β”‚                             │── json dump      β”‚
     β”‚                            β”‚                             │── SSLClient POSTβ–Άβ”‚
     β”‚                            β”‚                             β”‚                  │── INSERT row
     β”‚                            β”‚                             │◀── HTTP 201 ─────│
     β”‚                            β”‚                             β”‚                  β”‚
     β”‚                            β”‚                        [on failure]            β”‚
     β”‚                            β”‚                             │── sleep(3s)      β”‚
     β”‚                            β”‚                             │── retry (max 5x)β–Άβ”‚

🧠 Engineering Decisions

1. πŸ—‚οΈ Delta-Only File Watching (POSWatcher)

POSWatcher never re-reads the full CSV on each poll cycle. It stores the last-read byte offset in last_pos (std::streampos, initialized to 0) and calls seekg(last_pos) at the start of each 2-second iteration, reading only lines that have been appended since the previous pass. After draining new lines, tellg() advances the bookmark. A file.clear() call resets any EOF flags before the seek, which is essential for the re-open-per-cycle pattern used here.

The header row is explicitly skipped by checking for the string "Timestamp", ensuring it is never passed to parseLine() regardless of when the file is first opened.

// POSWatchdog.cpp
file.seekg(last_pos);                                    // Jump to last known position

string line;
while (getline(file, line)) {
    if (line.find("Timestamp") != string::npos) continue; // Skip CSV header
    if (line.empty()) continue;
    OrderInfo info = parseLine(line);
    q.push(info);
}

file.clear();
last_pos = file.tellg();                                 // Advance the bookmark

Why it matters: Re-scanning a growing log file on every 2-second poll wastes CPU and RAM on aging POS hardware. Delta reads keep resource usage near-zero regardless of log file size.


2. πŸ”’ Thread-Safe Buffer (utils.h / utils.cpp)

The Buffer class is the sole communication channel between POSWatcher (producer) and CloudUploader (consumer), each running on their own std::thread spawned in main.cpp. push() uses a std::lock_guard for scoped, exception-safe locking and calls cv.notify_one() to wake the consumer. pop() uses std::unique_lock with a cv.wait() predicate, blocking the consumer thread entirely when the queue is empty β€” eliminating CPU busy-waiting.

// utils.cpp
void Buffer::push(OrderInfo info) {
    std::lock_guard<std::mutex> lock(mtx);
    q.push(std::move(info));
    cv.notify_one();                              // Wake the blocked consumer
}

bool Buffer::pop(OrderInfo& info) {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [this] { return !q.empty(); }); // Sleep until data arrives
    info = std::move(q.front());
    q.pop();
    return true;
}

Why it matters: Without this, the watcher and uploader would either block each other on a single thread or race on an unprotected shared data structure, risking data corruption and dropped records.


3. 🌐 Retry Mechanism with Fixed Delay (CloudUploader)

sendToCloudWithRetry() wraps every HTTPS POST in a retry loop capped at 5 attempts. On any failure β€” whether a connection error (status 0) or an unexpected HTTP status β€” it logs the attempt number and status code, waits a fixed 3 seconds, and retries. Both HTTP 200 and 201 are treated as success, accommodating Supabase's 201 Created response on INSERT. If all 5 attempts are exhausted, a CRITICAL log line is emitted.

// CloudUploader.cpp
while (!success && current_try < max_retries) {
    current_try++;
    auto res = cli.Post("/rest/v1/orders", headers, payload, "application/json");

    if (res && (res->status == 201 || res->status == 200)) {
        success = true;
    } else {
        int status = res ? res->status : 0;
        std::cerr << "[Uploader] ERROR " << status << ". Attempt "
                  << current_try << "/" << max_retries << " failed. Retrying in 3s..." << endl;
        std::this_thread::sleep_for(std::chrono::seconds(3));
    }
}

if (!success) {
    std::cerr << "[Uploader] CRITICAL: Failed to send data after "
              << max_retries << " attempts. Data dropped." << endl;
}

Why it matters: Physical store Wi-Fi is unreliable. Without retry logic, any transient network blip would silently drop an order and corrupt BI metrics downstream in Metabase.


Built to bridge the gap between the hardware stores can't replace and the data infrastructure they deserve.


πŸ‘¨β€πŸ’» Author Etay De-Beer - B.Sc. Computer Science Student.

About

A high-performance C++ ETL agent that streams real-time data from legacy POS systems to cloud analytics.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages