A multi-tenant platform where each business manages its own stores, inventory, sales and staff, and can optionally turn on a public online storefront so its customers can browse products and place orders directly.
| Term | Meaning |
|---|---|
| Business | The tenant/company itself. Owns everything else. |
| Store | A physical/operational location belonging to one business. A business can have many. |
| Storefront | A business's online sales channel (guest checkout, orders, invoices) — not tied to a physical Store. |
| Path | What it is |
|---|---|
orbit-backend/ |
Express + MongoDB REST API |
orbit-frontend/ |
React/Vite app: admin dashboard, marketing site, business storefronts |
orbit-admin/ |
Electron desktop shell that loads the admin dashboard |
universal-main-frontend/ |
Experimental scaffold, not wired into the rest of the system |
graph TB
subgraph Clients
AdminUI["Admin Dashboard<br/>/admin/*"]
MarketingUI["Public Marketing Site<br/>/"]
StorefrontUI["Business Storefronts<br/>/store/:slug or slug.domain"]
Desktop["Electron Desktop Shell"]
end
API["orbit-backend<br/>Express REST API"]
Mongo[("MongoDB")]
Redis[("Redis")]
MPESA["M-Pesa Daraja API<br/>(STK Push, POS sales only)"]
AdminUI --> API
MarketingUI --> API
StorefrontUI --> API
Desktop --> AdminUI
API --> Mongo
API --> Redis
API --> MPESA
Tenant resolution — a storefront request resolves to a business in this order: /store/:slug path → ?store= query override (local dev) → subdomain of the hostname.
flowchart LR
V["Visitor requests a page"] --> P{"/store/:slug ?"}
P -- yes --> S1["Use :slug from path"]
P -- no --> Q{"?store= override?"}
Q -- yes --> S2["Use query override"]
Q -- no --> H{"Hostname has a subdomain?"}
H -- yes --> S3["Use subdomain as slug"]
H -- no --> M["No tenant resolved"]
S1 --> L["Look up Business by ecommerce.storeSlug"]
S2 --> L
S3 --> L
M --> FB["Render platform marketing pages"]
L --> F{"Found, enabled, published?"}
F -- yes --> R["Render that business's storefront"]
F -- no --> NA["'This business isn't available right now'"]
Checkout — stock decrement, order creation, and invoice creation happen in one MongoDB transaction, with an atomic conditional stock update so concurrent checkouts can't oversell, retried on write conflict:
sequenceDiagram
participant C as Customer
participant API as Storefront API
participant DB as MongoDB
C->>API: POST /storefront/:slug/checkout
API->>DB: Validate business + items (no writes)
loop up to 3 attempts
API->>DB: BEGIN TRANSACTION
API->>DB: decrementStockIfAvailable() per item
alt out of stock
API->>DB: ABORT
API-->>C: "item just sold out"
else write conflict (concurrent order)
API->>DB: ABORT
Note over API,DB: retry
else success
API->>DB: create Order + Invoice
API->>DB: COMMIT
API-->>C: 201 Order + Invoice
end
end
git clone https://github.com/Ritahchanger/orbit.git
cd orbit
cd orbit-backend && npm install && cd ..
cd orbit-frontend && npm install && cd ..orbit-backend/.env (see .env.example for the full list):
PORT=5000
MONGO_URI=mongodb://127.0.0.1:27017/orbitdb # run as a replica set — checkout uses transactions
JWT_SECRET=replace_me
FRONTEND_URL=http://localhost:5173orbit-frontend/.env:
VITE_API_BASE_URL=http://localhost:5000/api/v1/# orbit-backend/
node src/seeders/admin.seed.js
node src/permissions/seeders/permissions.seed.js
node src/permissions/seeders/roles.seed.js
npm run dev # http://localhost:5000
# orbit-frontend/
npm run dev # http://localhost:5173The superadmin account is created by admin.seed.js. Change the default credentials in that file before seeding anything outside local development.
To try a business's storefront locally, give it ecommerce.enabled, ecommerce.isPublished, and an ecommerce.storeSlug, then visit http://<storeSlug>.localhost:5173/products (any *.localhost subdomain resolves to 127.0.0.1 automatically).
Five tiers, seeded in permissions.seed.js / roles.seed.js: Staff (4, read-only) → Cashier (5) → Manager (7) → Admin (9) → Superadmin (10, everything). Full matrix in orbit-frontend/src/config/routes.config.jsx.
POST /auth/login
GET /business/ecommerce-settings Admin/Superadmin
PATCH /business/ecommerce-settings Admin/Superadmin
GET /storefront/:slug Public
GET /storefront/:slug/products Public
POST /storefront/:slug/checkout Public
GET /orders
GET /orders/customers
PATCH /orders/:id/status
GET /invoices
POST /invoices/standalone
POST /invoices/from-sale/:transactionId
All routes are mounted under /api/v1 — see orbit-backend/src/server.js for the full table.
deploy.sh— rsyncs backend + built frontend to a remote server over SSHorbit-backend/ecosystem.config.js— PM2 process config (pm2 start ecosystem.config.js --env production)orbit-frontend:npm run build, servedist/behind any reverse proxy
For subdomain-based storefronts in production: point wildcard DNS (*.your-domain.com) at the server and make sure the reverse proxy forwards the original Host header — CORS and tenant resolution both depend on it.
No automated test suite yet. Verification has been manual (node --check, direct curl calls including deliberate concurrent-request races against checkout, vite build).
- No idempotency key on checkout — a retried/duplicated request can create a duplicate order
- No payment gateway on storefront checkout — the payment method is recorded, not processed. (M-Pesa STK Push is wired up only for in-store POS sales via
/api/v1/mpesa.) - No custom-domain support for storefronts (subdomain-of-platform-domain only)