BadShuffle is a self-hosted event rental software platform for inventory management, project quoting, contract approvals, billing, messaging, files, and SEO-friendly public catalog publishing. It targets event rental operators who want ownership of their stack, local-first deployment options, and a client-facing workflow without SaaS lock-in.
BadShuffle is designed for event rental companies that need project quoting, rental inventory management, pull sheets, customer approvals, internal team coordination, and self-hosted operational control in one system.
SEO keywords: self-hosted event rental software, rental inventory management software, event rental CRM, quote management system, client portal for rentals, pull sheet software, event operations platform, React Express SQLite Rust app.
Pre-release (0.x). See CHANGELOG.md for version history and RELEASE_NOTES_0.0.12.md for the current release summary.
- Real product, not a toy clone — BadShuffle covers inventory, projects, pricing, approvals, signatures, files, messages, public catalog pages, and operations/admin tooling in one coherent workflow.
- Full-stack product ownership — The repo includes a React/Vite frontend, Express API, SQLite/sql.js persistence, a Chrome extension, Docker deployment, Windows packaging, and an in-app update path.
- Complex operational domain — Signed-vs-unsigned quote changes, inventory conflicts, rental sections, contract artifacts, quote-linked messaging, and public catalog SEO all target real event-rental problems.
- Portfolio-grade engineering signals — Safe startup migrations, route-level code splitting, shared totals logic, audit-oriented signing artifacts, structured release notes, and handoff docs show maintainability discipline, not just feature velocity.
v0.0.12 is the operations, AI, and engine release. It expands BadShuffle beyond the earlier visibility/dashboard work with real internal coordination tooling, deeper inventory operations, AI-assisted workflows, and a guarded Rust engine layer.
- Rust engine foundation — Added a dedicated
rust-core/workspace plus parity tooling, lifecycle controls, and guarded Node integration for inventory availability/conflicts and pricing. - Quote Assistant + AI platform — Added quote-scoped assistant workflows, shared AI provider/model controls, managed local Ollama support, and managed/external Onyx support for internal AI features.
- Team communication + notifications — Added live notifications, notification settings, Team Groups, and Team Chat so operators can coordinate inside the product.
- Inventory operations upgrade — Added set-aside workflows, stronger mobile/desktop inventory controls, AI batch editing, exact/loose search, serial numbers, QR-backed scan identity, and product sales totals.
- Pull sheets and internal picking — Added quote pull sheets, aggregate pull exports for overlapping jobs, and scan-addressable internal pick views.
- Admin, help, and appearance — Added deeper runtime controls in Admin, a guided Help page, and a dedicated Appearance settings page.
- Self-hosted event rental software — Built for rental companies that want inventory, quotes, files, public catalog, and team operations without SaaS lock-in.
- Inventory and pull-sheet workflow — Supports inventory search, set-asides, batch editing, internal pull sheets, aggregate pulls, and QR/serial tracking for operational fulfillment.
- Quote and approval workflow — Covers project quoting, contract approvals, signatures, public quote sharing, billing context, and quote-linked messaging.
- Internal operations and AI — Adds Team Chat, live notifications, AI-assisted quote workflows, managed local AI options, and operator-oriented admin diagnostics.
- Inventory management — Searchable catalog with photos, categories, subrental support, vendor links, associations, and accessory relationships.
- Project workflow — Event projects/quotes, custom items, price overrides, line-item discounts, adjustments, contract text, approvals, signatures, and public sharing.
- Availability awareness — Quote conflict checks, oversold detection, subrental needs, inventory-aware quote building, and a guarded Rust availability engine path.
- Fulfillment + operations — Fulfillment rows, item check-in, internal fulfillment notes, persistent staff presence, and a team workspace.
- Maps + analytics — Quote geocoding, operator map views, sales pipeline reporting, and product sales/date-window statistics.
- Public-facing surfaces — Client quote page, live quote messaging, SEO catalog pages,
robots.txt, sitemap generation, and JSON-LD metadata. - Comms and files — SMTP send, IMAP reply capture, media library uploads, and quote-linked attachments.
- Operations + admin — Module permissions, role-aware navigation, user approval/admin tools, update controls, SQLite backup/restore, notifications, team groups/chat, and directory-style client/vendor/venue management.
- Import and sync — Google Sheets import plus a Chrome extension for syncing items from Goodshuffle Pro, with manual JSON fallback import.
- Optional AI — Per-feature provider settings for OpenAI, Anthropic, Gemini, managed local Ollama, and Onyx-backed internal workflows without making AI a hard dependency.
- Shipping a product with both internal tools and customer-facing flows.
- Evolving a live schema with safe startup migrations.
- Balancing delivery speed with deployment portability.
- Writing software that can be evaluated as both a business tool and an engineering portfolio piece.
- Demonstrating product judgment, not just coding throughput: many recent changes improve auditability, safety, discoverability, and operator clarity.
- Permission follow-through — Finish moving remaining legacy route/page checks onto the shared permission system.
- Fulfillment depth — Extend pull sheets and fulfillment into deeper operational flows such as staged pull/return handling.
- Accessibility and responsiveness — Finish the cross-theme QA, mobile pass, and keyboard/focus improvements still tracked in the AI docs.
- Compliance hardening — Continue improving contract-signing evidence capture while separating technical auditability from formal legal compliance claims.
- Engine rollout hardening — Keep Rust parity, pricing, and local AI runtime controls stable before widening those domains further.
More context lives in AI/TODO.md, AI/HANDOFF.md, and the coordination docs under [AI/].
BadShuffle integrates and builds on a number of strong open-source and external platforms. Credit is due to the projects that make the current product possible:
- React and Vite for the client application and build workflow
- Express and sql.js / SQLite for the current application and data layer
- Rust for the guarded engine rollout in
rust-core/ - Mapbox for the maps workspace and geocoding-linked operator views
- Onyx for internal team knowledge/chat workflows
- Ollama for managed local language model support
- OpenAI, Anthropic, and Gemini provider integrations for optional hosted AI features
- bwip-js for built-in QR/barcode generation
- Sharp for image processing and optimization
- IMAPFlow and Nodemailer for email polling and outbound mail
These integrations are part of the product story, not an afterthought, and they deserve explicit credit.
badshuffle/
├── server/ Express API + sql.js SQLite (port 3001)
│ ├── index.js
│ ├── db.js DB bootstrap + persistence orchestration
│ ├── db/ schema, migrations, defaults, queries, repositories
│ ├── routes/ auth, quotes, items, files, maps, sales, team, settings, admin, publicCatalog
│ ├── services/ quote domain services, analytics, geocoding, files, diagnostics, IMAP
│ └── lib/ auth, permissions, file signing, crypto, image proxy
├── client/ React + Vite SPA (port 5173 in dev)
│ ├── src/
│ │ ├── pages/ Dashboard, Maps, Inventory, Quotes, QuoteDetail, Team,
│ │ │ Profile, Files, Messages, Settings, PublicCatalog, PublicItem
│ │ ├── features/sales-dashboard/
│ │ └── components/
│ └── serve.js Zero-dep static server used by the packaged exe
├── extension/ Chrome MV3 extension (load unpacked)
├── AI/ Release planning, API docs, audits, and agent notes
├── ai/ Current-state architecture, workflows, and handoff docs
├── Dockerfile Multi-stage build (Bun → client, Node:20 → server)
├── docker-compose.yml
└── scripts/
| Requirement | Version | Notes |
|---|---|---|
| Bun | 1.1+ | Recommended for repo scripts and dev flow |
| Node.js | 20+ | Fine for general local tooling and fallback runtime |
| Node.js | 14.x | Only required for current pkg executable targets |
| Docker | 24+ | Optional |
| Chrome | Current | Optional, for the extension |
The scripted dev flow assumes Bun is installed. The packaged
.exebuild still targets Node 14 because ofpkg.
git clone https://github.com/248Tech/badshuffle.git
cd badshuffle
npm run install:allIf you do not have Bun yet, install Bun first or fall back to:
npm install
npm install --prefix server
npm install --prefix clientCopy .env.example to .env, then set the values you care about:
PORT=3001
APP_URL=http://localhost:3001
OPENAI_API_KEY=sk-...
MAPBOX_ACCESS_TOKEN=pk-...APP_URL matters for public catalog canonicals, signed file URLs, and sitemap output.
MAPBOX_ACCESS_TOKEN is optional unless you want the Maps workspace. If it is unset, map/geocode features should be treated as unavailable.
Local development
npm run dev- API:
http://localhost:3001 - Client:
http://localhost:5173
LAN / device testing
npm run dev:hostUse http://<your-pc-ip>:5173 from another device on the same network.
Containerized development
npm run dev:dockerProduction-style Docker run
docker compose up -d --buildThat serves the API and built client from http://localhost:3001 with DB/uploads persisted in the badshuffle_data volume.
In Vite dev mode, BadShuffle can auto-create and log in a local admin account through /api/auth/dev-login.
- Email:
admin@admin.com - Password:
admin123
That route is disabled when NODE_ENV=production.
Outside dev mode, run:
npm run create-admin -- --email you@example.com --password changeme123Then sign in, open Settings, and configure the modules you plan to use first:
- Company/app identity
- SMTP/IMAP if you want outbound/inbound messaging
- AI provider keys if you want suggestions
- Mapbox token if you want the Maps workspace
- Create or import inventory.
- Create a project from the Projects page and add quote sections/items.
- Send the quote, review the public quote page, and test approval/signature.
- Confirm the project and review the Fulfillment tab.
- Open Team, Maps, and Dashboard to verify your operational surfaces.
- Open
chrome://extensions - Enable Developer mode
- Click Load unpacked
- Select the
extension/folder - Visit Goodshuffle Pro and use the sync action
Locked out? Run npm run create-admin -- --email your@email.com --password yournewpassword.
Frontend can’t reach the API? If /api/auth/login fails with ECONNREFUSED, start both services from the repo root with npm run dev or npm run dev:host.
Maps page is blank or missing data? Confirm MAPBOX_ACCESS_TOKEN is set and that quotes have valid venue/client addresses for geocoding.
Why is badshuffle.lock missing from git? It is a local runtime file used to communicate the active dev port and should stay uncommitted.
Go to Settings in the sidebar to configure email.
Fill in your SMTP host, port, credentials, and "from" address. BadShuffle uses these when you click Send to Client on a quote. The sent email is logged in the Messages page.
BadShuffle can poll your inbox for client replies and automatically link them to the original quote thread.
| Setting | Description |
|---|---|
| IMAP Host | e.g. imap.gmail.com |
| Port | 993 (TLS) or 143 (STARTTLS) |
| Secure | TLS/SSL (port 993) or STARTTLS |
| Username / Password | Your email credentials or app password |
| Enable auto-poll | Polls every 5 minutes when enabled |
Only emails that are direct replies to a sent quote (In-Reply-To header match) are ingested.
Gmail users: Create an App Password — regular passwords are blocked by Google for SMTP/IMAP access.
| Setting | Description |
|---|---|
| Count out-of-stock as conflicts | count_oos_oversold — When enabled, out-of-stock items are included in dashboard conflict detection (Conflicts and Subrental Needs panels). Configure in Settings. |
The Files page lets you upload images, PDFs, and documents to BadShuffle's local uploads/ folder.
- Drag-and-drop or click-to-pick (up to 20 files, 50 MB each)
- Filter by Images / Documents
- Copy the direct serve URL for use in emails or custom items
- Files are served publicly at
/api/files/:id/serveso<img>tags work without authentication
When sending a quote email, select attachments from the file picker in the Send modal.
On any quote detail page, use + Add custom item to add a one-off line item not in your inventory:
- Name, price, quantity, taxable flag
- Pick a photo from the Files library or from inventory item photos
- Custom items appear in quote totals and in the PDF/image export
- Selecting an inventory item image pre-fills the price from that item
Single-container production deployment. DB and uploads persist in a named volume.
docker compose up -dOr build manually:
docker build -t badshuffle .
docker run -p 3001:3001 -v badshuffle_data:/data badshuffleCopy .env.example → .env and set APP_URL to your public hostname (used for sitemap, canonical URLs, and signed file URLs):
APP_URL=https://catalog.yourcompany.com
OPENAI_API_KEY=sk-... # optionalThe container serves the API on port 3001 and also serves the built React SPA. Visit http://localhost:3001 (or your mapped port) in your browser.
badshuffle.dbanduploads/are stored at/datainside the container, mounted from thebadshuffle_datanamed volume.
Produces two .exe files that run on Windows without Node.js installed.
npm run packageOutput in dist/:
dist/
├── badshuffle-server.exe Express API
├── badshuffle-client.exe Static file server (opens browser automatically)
├── www/ Built React SPA
├── .env.example
└── START.bat Launches both exes in sequence
First run downloads ~30 MB Node 14 win-x64 binary to ~/.pkg-cache (cached for future builds).
- Copy the
dist/folder anywhere on the target machine - (Optional) Copy
.env.example→.envand add any AI provider keys you want - Double-click
START.bat(or run each exe separately) - The browser opens to
http://localhost:5173automatically
badshuffle.dbis created next to the server exe and persists between runs. Anuploads/folder is created in the same directory for file storage.
All endpoints are prefixed with /api. Protected endpoints require Authorization: Bearer <token>.
| Method | Path | Description |
|---|---|---|
| GET | /items |
List items (?search=, ?hidden=, ?category=) |
| POST | /items |
Create item |
| POST | /items/upsert |
Create or update by title (used by extension) |
| POST | /items/bulk-upsert |
Bulk create/update by title (used by Extension JSON import) |
| PUT | /items/:id |
Update item |
| DELETE | /items/:id |
Delete item |
| GET | /items/:id/associations |
Get related items |
| POST | /items/:id/associations |
Add association |
| DELETE | /items/:id/associations/:child |
Remove association |
| GET | /items/:id/accessories |
List saved accessory links for an item |
| POST | /items/:id/accessories |
Add an accessory link |
| DELETE | /items/:id/accessories/:accessoryId |
Remove an accessory link |
| Method | Path | Description |
|---|---|---|
| GET | /quotes |
List quotes (?search=, ?status=, ?event_from=, ?event_to=, ?has_balance=1, ?venue=) |
| POST | /quotes |
Create quote |
| GET | /quotes/:id |
Get quote with items and custom items |
| PUT | /quotes/:id |
Update quote |
| DELETE | /quotes/:id |
Delete quote |
| POST | /quotes/:id/duplicate |
Duplicate quote (details, items, custom items); returns new quote |
| POST | /quotes/:id/send |
Send quote email, set status to sent, log message |
| POST | /quotes/:id/approve |
Set status to approved |
| POST | /quotes/:id/revert |
Revert to draft |
| POST | /quotes/:id/items |
Add inventory item to quote |
| PUT | /quotes/:id/items/:qitemId |
Update quote item (quantity 0 removes it) |
| DELETE | /quotes/:id/items/:qitemId |
Remove quote item |
| PUT | /quotes/:id/items/reorder |
Update line-item order |
| POST | /quotes/:id/custom-items |
Add custom line item |
| PUT | /quotes/:id/custom-items/:cid |
Update custom item (quantity 0 removes it) |
| DELETE | /quotes/:id/custom-items/:cid |
Remove custom item |
| GET | /quotes/public/:token |
Public quote view (no auth) |
| GET | /quotes/public/:token/messages |
Public quote thread (no auth) |
| POST | /quotes/public/:token/messages |
Post client message to quote thread (no auth) |
| Method | Path | Description |
|---|---|---|
| GET | /templates |
List email templates |
| POST | /templates |
Create email template |
| PUT | /templates/:id |
Update email template |
| DELETE | /templates/:id |
Delete email template |
| GET | /templates/contract-templates |
List contract templates |
| POST | /templates/contract-templates |
Create contract template |
| DELETE | /templates/contract-templates/:id |
Delete contract template |
| GET | /templates/payment-policies |
List payment policies |
| POST | /templates/payment-policies |
Create payment policy |
| PUT | /templates/payment-policies/:id |
Update payment policy |
| DELETE | /templates/payment-policies/:id |
Delete payment policy |
| GET | /templates/rental-terms |
List rental terms |
| POST | /templates/rental-terms |
Create rental terms |
| PUT | /templates/rental-terms/:id |
Update rental terms |
| DELETE | /templates/rental-terms/:id |
Delete rental terms |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /files |
Required | List uploaded files |
| POST | /files/upload |
Required | Upload files (multipart, up to 20 files) |
| GET | /files/:id/serve |
Public | Stream file inline (for <img> tags) |
| DELETE | /files/:id |
Required | Delete file from disk and DB |
| Method | Path | Description |
|---|---|---|
| GET | /messages |
List messages (?quote_id=, ?direction=) |
| GET | /messages/unread-count |
Count of unread inbound messages |
| PUT | /messages/:id/read |
Mark message as read |
| DELETE | /messages/:id |
Delete message |
| Method | Path | Description |
|---|---|---|
| GET | /availability/conflicts |
Items where reserved quantities exceed stock |
| GET | /availability/subrental-needs |
Items requiring subrental due to shortfall |
| GET | /availability/quote/:id |
Conflict check for a specific quote |
| GET | /availability/quote/:id/items?ids=1,2,3 |
Stock + reserved counts for specific items on that quote's date range |
| Method | Path | Description |
|---|---|---|
| GET | /vendors |
List vendors |
| POST | /vendors |
Create vendor |
| PUT | /vendors/:id |
Update vendor |
| DELETE | /vendors/:id |
Delete vendor |
| Method | Path | Description |
|---|---|---|
| GET | /updates |
Current version + cached update check status |
| GET | /updates/releases |
Fetch GitHub releases and flag newer tags |
| POST | /updates/apply |
Download selected release assets and restart (packaged .exe mode only) |
| Method | Path | Description |
|---|---|---|
| GET | /catalog |
Server-rendered SEO catalog page (HTML) |
| GET | /catalog/item/:id |
Server-rendered SEO item detail page (HTML) |
| GET | /robots.txt |
robots.txt (allows /catalog, disallows internal routes) |
| GET | /sitemap.xml |
XML sitemap (catalog, category pages, all item URLs) |
| GET | /api/public/catalog-meta |
Company info, categories, counts, total item count |
| GET | /api/public/items |
Item list (?category=, ?search=, ?limit=, ?offset=) |
| GET | /api/public/items/:id |
Single public item detail |
| Method | Path | Description |
|---|---|---|
| GET | /health |
Health check |
| GET | /stats |
Aggregate usage stats |
| GET | /stats/:itemId |
Per-item stats |
| POST | /sheets/preview |
Preview a Google Sheet import |
| POST | /sheets/import |
Execute sheet import |
| POST | /ai/suggest |
Get AI item suggestions for a quote |
| GET | /proxy-image?url= |
Proxy external images (bypasses CORS) |
| GET | /settings |
Get all settings |
| PUT | /settings |
Update settings |
| POST | /settings/test-imap |
Test IMAP connection |
| GET | /v1/docs |
Swagger UI for the versioned API |
| GET | /v1/openapi.json |
Raw OpenAPI document |
Client helpers in client/src/api.js: getVendors, getConflicts, getQuoteAvailabilityItems, reorderQuoteItems, getPaymentPolicies, getRentalTerms, getItemAccessories, bulkUpsertItems, getUpdateStatus, getUpdateReleases, applyUpdate, getPublicMessages, sendPublicMessage.
| Layer | Technology |
|---|---|
| Server | Bun (dev) / Node.js (packaged), Express |
| Database | SQLite via sql.js (pure WASM — no native build) |
| Client | React 18, React Router 6, Vite 3 |
| Email send | nodemailer |
| Email receive | imapflow + mailparser |
| Extension | Chrome MV3 (no build step) |
| Packaging | pkg 5.8.1 |
| AI | Optional OpenAI, Anthropic, and Gemini provider integrations |
sql.js is used instead of better-sqlite3 because it requires no Python / node-gyp compilation, making it portable across machines without a C++ build toolchain.
- The Vite dev server proxies
/apito the server port fromPORTorbadshuffle.lock(falls back tohttp://localhost:3001) - Production builds bake in
VITE_API_BASE=http://localhost:3001viaclient/.env.production - The
server/db.jsshim exposes a synchronous API matchingbetter-sqlite3so routes don't need to be aware of async WASM initialization - DB is saved to disk after every write (or after each transaction commit)
- All DB migrations use
try/catch+CREATE TABLE IF NOT EXISTS/ALTER TABLEso they're safe to run on every startup against existing databases - Uploaded files are stored in
uploads/next to the project root (dev) or next to the server exe (packaged); the directory is auto-created on startup - The IMAP poller only runs when
imap_host,imap_user, andimap_poll_enabled=1are all set - Initial Bun support has been tested in the dev workflow (server runs under
bun index.jsin dev); packaging and production continue to use Node
MIT. See LICENSE.