Skip to content
basic-automationPublic

About

Desktop app for managing one product catalog and syncing it across eBay, Squarespace, XMR Bazaar and Amazon. Rust + Tauri 2 + Nuxt 3, with sandboxed vendor plugins and optional P2P sync over Tor.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Nisaba

A desktop app for managing one product catalog and keeping it in sync across multiple selling platforms.

Nisaba holds a local master catalog of products and variants, syncs inventory, pricing, descriptions and photos to each connected marketplace, imports supplier catalogs through sandboxed vendor plugins, and can sync between installs over Tor.

Built with Rust + Tauri 2 and a Nuxt 3 frontend.

Screenshots

Captured from the real app running a fixture install (sample catalog data — no customer data anywhere). The capture is part of the CI UI smoke test (scripts/ui-smoke.py --screenshot ... --seed-sample-catalog).

Dashboard Products Listings
Dashboard Products Listings
Vendors Inventory
Vendors Inventory

Features

  • Multi-platform sync — eBay, Squarespace, XMR Bazaar and Amazon behind one PlatformAdapter trait, each declaring its own PlatformCapabilities so the UI and sync engine only attempt what a platform actually supports.
  • Inventory reconciliation — a sync engine with quantity deltas, conflict resolution, and sale detection for platforms that use stock modes (unlimited / one-time) instead of numeric quantities. It also has a dry-run mode that resolves a full cycle and reports every change it would make without writing to any platform — or to the local database.
  • Listing management — descriptions, photos, and multi-tier pricing, edited locally and published per platform.
  • Vendor plugins — TypeScript modules run in an embedded Deno runtime to import supplier catalogs (SKUs, variants, dealer pricing, inventory). Ships with a Rothco Wholesale plugin.
  • Multi-company + P2P sync — optional peer-to-peer sync between installs over Tor onion services, with AES-GCM encrypted payloads and secrets held in the OS keyring.
  • Analytics — per-product history and time-windowed analytics. History is shown as entry counts in the UI today (inventory.vue renders history.length); charting the entries is still on the roadmap (Phase 1).

Sync cycle

Each cycle polls every enabled platform, resolves a canonical quantity per product (per variant, where variants are mapped) from the summed deltas against the last-known snapshots, and pushes the resolved quantity back to any platform whose reading disagrees.

Partial-failure policy — never push to an unpolled platform. A platform with no successful reading this cycle receives no pushes this cycle. For a numeric platform that means a poll that returned the listing's quantity; for a stock-mode platform (XMR Bazaar) it means a successful poll and a successful sale-detection pass, since sale detection is that platform's reading. One platform being down does not abort the cycle — the others still resolve and sync, and the failure is reported as a platform error — but the unread platform is never written to: pushing a canonical value resolved without any reading from it could overwrite live stock on a platform that was merely slow to answer. It is reconciled on the next cycle in which it can be read again.

Repository layout

crates/
  core/                 Shared types, config, DB (turso), sync engine, conflict
                        resolution, analytics, crypto
  platform-ebay/        eBay adapter (OAuth)
  platform-squarespace/ Squarespace adapter (API key)
  platform-xmrbazaar/   XMR Bazaar adapter (HTML form scraping + cookies/CSRF)
  platform-amazon/      Amazon SP-API adapter (LWA OAuth)
  p2p/                  Tor onion service, peer client/server, company sync
  vendor-runtime/       Deno-based sandbox that executes vendor plugins
  tauri-app/            Tauri backend — ~96 commands exposed to the frontend
frontend/               Nuxt 3 SPA (Tailwind + shadcn-vue)
plugins/
  rothco-wholesale/     First-party vendor plugin (GraphQL v2 API)
  template/             Starter plugin and contract reference for third parties
migrations/             SQL migrations, applied in order
config.example.toml     Template for the runtime config

The Tauri layer is the only place the frontend talks to; commands are grouped by domain in crates/tauri-app/src/commands (products, listings, sync, vendors, pricing, photos, analytics, company, auth, config, logs, export_import, images).

Getting started

Prerequisites

  • Rust — the toolchain is pinned to stable by rust-toolchain.toml, matching CI, so rustup selects it for you. Plus the Tauri 2 prerequisites for your OS.
  • Node.js ^20.19.0 || >=22.12.0 and npm — required by Nuxt 3.21 / Vite 7 and enforced by the engines field in frontend/package.json
  • cargo-tauri CLI: cargo install tauri-cli --version "^2"

Development

cd frontend && npm install
cargo tauri dev

cargo tauri dev starts the Nuxt dev server itself (port 3456, chosen to avoid conflicts) and then launches the desktop shell against it.

Debug builds run in sandbox mode:

  • One-click sign-in goes through the sandbox broker (nisaba-auth-sandbox.basicautomation.io, which holds eBay's sandbox keyset and Amazon's sandbox app).
  • eBay and Amazon calls go to their sandbox hosts.
  • Config, catalog and tokens live in a separate nisaba-sandbox data folder.
  • A banner says so on every page.

Release builds never do. NISABA_SANDBOX=0 runs a debug build against the real marketplaces and data folder, and NISABA_SANDBOX=1 turns sandbox mode on in any build. Amazon's SP-API sandbox only returns Amazon's canned test responses, so it checks sign-in and request shapes, not real listings.

If your global ~/.cargo/config.toml sets nightly-only flags in build.rustflags (-Z threads=8 is a common one), every cargo command here fails with "the option Z is only accepted on the nightly compiler" — the stable pin and those flags cannot coexist. Cargo merges rustflags arrays across config files rather than overriding them, so a repo-local config cannot clear it; the environment variable can:

RUSTFLAGS="" cargo build --workspace

A note on fonts

The UI is designed around four licensed Adobe Fonts (Clother, Clarendon Wide Sketch, Pulpo Rust, Jubilat). Those .woff2 files are not redistributed with this repository and are gitignored. The app falls back to system font stacks and renders correctly without them — just not on-brand.

If you have a license, drop your own .woff2 files into frontend/public/fonts/ using the filenames listed in frontend/assets/css/fonts.css and they are picked up on the next build.

Production build

cargo tauri build

This runs npm run generate in frontend/ and bundles the static output into the app.

UI smoke test (Linux)

scripts/ui-smoke.py drives the real desktop app over WebDriver and checks what the plugin marketplace renders:

cargo install tauri-driver
cargo build -p nisaba-tauri --bin nisaba-tauri --example ui_fixture
scripts/ui-smoke.py --target-dir "$(cargo metadata --format-version 1 | jq -r .target_directory)"

It seeds a throwaway install in a temp directory (every platform disabled) and runs the app from there inside a network namespace with only loopback up, so it never sees your real config or data and cannot reach any network. It needs WebKitWebDriver (from webkitgtk), a desktop session, and unprivileged user namespaces.

Configuration

Config lives outside the repo, at the platform data directory:

  • Windows: %APPDATA%\nisaba\config.toml
  • Unix: ~/.config/nisaba/config.toml

A config.toml in the process's working directory takes precedence over both of the above — useful for development and tests. See resolve_and_load in crates/core/src/config.rs: an explicit CLI path wins first, then ./config.toml in the working directory, then the data-directory file.

A default file is written on first run if none exists. Copy config.example.toml as a starting point — it covers the sync schedule, database path, per-platform credentials, low-stock alert thresholds, and the P2P company settings.

Products are not configured in TOML; they live in the database and are managed through the UI. config.toml and *.db files are gitignored.

The database is SQLite-compatible (via turso), defaulting to nisaba.db in the data directory. Schema changes go in migrations/ as a new numbered file.

Vendor plugins

A vendor plugin is a TypeScript module executed in a locked-down Deno runtime — no ambient network or filesystem access, only what Nisaba injects as a Nisaba global: Nisaba.fetch, Nisaba.sleep, Nisaba.log.{trace,debug,info,warn,error}, and Nisaba.emitBatch for streaming listings back to the host as they are scraped. console.log and friends write to the same log as Nisaba.log.

A plugin exports two things:

export const metadata = {
  name: 'Rothco Wholesale',
  version: '2.5.0',
  description: '…',
  category: 'vendor',
  config_fields: [
    { key: 'api_token', label: 'API Token (Bearer)', required: true, secret: true },
  ],
  allowed_hosts: ['www.rothco.com'],
}

export async function fetchListings(
  config: Record<string, string>
): Promise<any[]> { /* … */ }

Metadata is read without running fetchListings, so the app can show a plugin's config fields before it is ever executed. secret: true masks a field's input; the values themselves are currently stored unencrypted in the app's database and shared with the company's P2P peers (moving them to the OS keyring is on the roadmap). Plugins may be single-file or multi-file.

Writing a plugin: start from plugins/template. It is a working plugin whose comments are the contract reference — the Nisaba API, every metadata and config_fields option, the listing schema (including variants and the extras keys the app recognises), paging, batching, rate-limit retries, and when to throw versus log and skip. plugins/rothco-wholesale is a real-world example that pages a GraphQL catalog.

What the sandbox enforces:

  • Filesystem — none. A plugin can import only its own files; imports that resolve outside its directory are refused, and a plugin's file names must be plain relative paths, so installing one cannot write outside the directory it is unpacked into.
  • Host APIs — only Nisaba. The Deno global is removed before plugin code runs.
  • Network — Nisaba.fetch is the only way out. A plugin lists the hosts it needs in its metadata as allowed_hosts: ['api.example.com', '*.cdn.example.com'], and fetches (and redirects) anywhere else are refused; [] means no network at all. A plugin that declares no list can still reach any host — declare one. Code at a module's top level runs when the plugin is installed and has no network access.
  • Resources — each fetchListings run gets 30 minutes of wall-clock time, a 1 GiB heap, 256 MiB of listing output (every emitBatch plus the return value) and 64 MiB per Nisaba.fetch response body; a metadata read gets 10 seconds, 128 MiB, and 1 MiB for each of the other two. A plugin that exceeds one is stopped with an error naming the limit; it cannot hang or crash the app. The time limit can be raised per plugin (up to 12 hours) on its card under Marketplace → Installed.

P2P company sync

Experimental / Preview. Company sync is experimental and not part of the 1.0 promise: 1.0 ships as a single-machine product, and company sync sits behind a "Preview" switch that is off by default. Today's Tor-based sync does not work end to end; Sync v2 (see ROADMAP.md, Phase 5) will replace the transport and data layer after 1.0.

Each company an install belongs to publishes a Tor onion service and syncs catalog state with its peers on an interval. ([company].enabled in config.toml is currently not read — the onion service starts for every company regardless; see the roadmap.)

The sync payload is JSON sent over the onion connection, so it is protected by Tor's end-to-end encryption; peers authenticate with the shared company secret, which is kept in the OS keyring. There is no additional application-layer encryption of the payload, and it includes platform auth tokens and plugin configuration. The company's platform configuration is stored AES-GCM-encrypted (key derived from the secret with HKDF). See crates/p2p.

Merge policy. Each section of the payload merges independently: products, variants, mappings, auth tokens, the company config and logo, and registry plugins are last-writer-wins on updated_at; a deleted mapping is a tombstone that propagates; platform snapshots keep the newer last_polled_at; processed XMR orders are a union. Peers that hold the same data converge — a repeated sync changes nothing (crates/p2p/tests/round_trip.rs).

Trust model — read this before adding a peer. The company secret is the only credential. Anyone who holds it is, in effect, a full member: they receive the whole payload (platform auth tokens and plugin configuration included), and whatever they send is merged. Beyond that, the current protocol trusts what a peer says about itself:

  • A peer's identity is the sender_peer_id/sender_onion it puts in its own request, so a secret holder can speak as any authorized peer — and doing so updates that peer's recorded onion address. /api/address-update can repoint any peer.
  • Admin-only endpoints (adding, approving and removing peers, changing roles) check the receiving node's role, not the caller's, so any secret holder can call them on an admin's node.
  • updated_at comes from the sender. Rows stamped more than 10 minutes ahead of the receiver's clock, or with an unparseable stamp, are refused; within that window a peer with a fast clock still wins ties.
  • Secret rotation is not implemented yet.

Share the secret only with installs you would give your marketplace credentials to. The fixes are on the roadmap (Phase 5).

Status

Pre-1.0 and under active development: the sync engine has not yet been exercised against all four platforms in production. 0.x releases are published as normal GitHub releases (not pre-releases) because the in-app updater only sees GitHub's "latest" release. A version with a suffix, such as v0.3.0-beta.1, is a pre-release.

ROADMAP.md is the single source of truth for planned work: a phase-ordered checkbox queue covering test coverage, the platform capability gaps, the plugin platform, P2P, and the release pipeline. Known gaps worth calling out up front:

  • No auto-update yet — the Tauri updater ships with an empty signing key.
  • Photo upload is unimplemented on every platform.
  • Test coverage is uneven. The sync engine, the database migrations, XMR Bazaar's HTML scraping, all four adapters' mapping layers and the vendor plugin runtime and sandbox have tests; every adapter's live network path and the P2P layer do not.

Security

Found a vulnerability? Please report it privately through this repository's Security tab rather than opening an issue — SECURITY.md has the details, including what counts (credential handling, the vendor plugin sandbox, the P2P layer, and adapters' handling of untrusted marketplace responses) and what does not.

PRIVACY.md states what leaves the machine (update checks, marketplace API calls, P2P relays) and what never does; the per-platform API terms review lives in docs/roadmap/platform-terms-review.md.

License

MIT

About

Desktop app for managing one product catalog and syncing it across eBay, Squarespace, XMR Bazaar and Amazon. Rust + Tauri 2 + Nuxt 3, with sandboxed vendor plugins and optional P2P sync over Tor.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages