Cheeblr
Functional Reactive Purescript Deku/Hyrule PoS System for Cannabis Dispensaries
Install / Use
/learn @harryprayiv/CheeblrREADME
Cheeblr: Cannabis Dispensary Management System
A full-stack cannabis dispensary point-of-sale and inventory management system built with PureScript (frontend) and Haskell (backend) on PostgreSQL — emphasizing type safety, functional programming, and reproducible builds via Nix.
Documentation
- Frontend Documentation — PureScript/Deku SPA architecture, async loading pattern, page modules, services
- Backend Documentation — Haskell/Servant API, database layer, transaction processing, inventory reservations
- Nix Development Environment — Setup, TLS, sops secrets management, service scripts, and test suite
- Dependencies — Project dependency listing
- To Do list — Planned features and optimizations
- Security Recommendations — Planned security and authentication upgrades
Features
Inventory Management
- Comprehensive Product Tracking: Detailed cannabis product data including strain lineage, THC/CBD content, terpene profiles, species classification, and Leafly integration
- Real-Time Inventory Reservations: Items are reserved when added to a transaction cart, preventing overselling during concurrent sessions. Reservations are released on item removal or transaction cancellation, and committed on finalization
- Role-Based Access Control: Dev-mode auth system with four roles (Customer, Cashier, Manager, Admin) and 15 granular capabilities governing inventory CRUD, transaction processing, register management, and reporting access
- Flexible Sorting & Filtering: Multi-field priority sorting (quantity, category, species) with configurable sort order and optional out-of-stock hiding
- Complete CRUD Operations: Create, read, update, and delete inventory items with full strain lineage data
- GraphQL Inventory API: Inventory queries available via
/graphql/inventoryusingmorpheus-graphql(backend) andpurescript-graphql-client(frontend), scoped to read-only inventory access
Point-of-Sale System
- Full Transaction Lifecycle: Create → add items (with reservation) → add payments → finalize (commits inventory) or clear (releases reservations)
- Parallel Data Loading: The POS page loads inventory, initializes the register, and starts a transaction concurrently using the frontend's
parSequence_pattern; degrades gracefully toTxPageDegradedstate on partial load failure - Multiple Payment Methods: Cash, credit, debit, ACH, gift card, stored value, mixed, and custom payment types with change calculation
- Tax Management: Per-item tax records with category tracking (regular sales, excise, cannabis, local, medical)
- Discount Support: Percentage-based, fixed amount, BOGO, and custom discount types with approval tracking
- Automatic Total Recalculation: Server-side recalculation of subtotals, taxes, discounts, and totals on item/payment changes
Financial Operations
- Cash Register Management: Open registers with starting cash, close with counted cash and automatic variance calculation
- Register Persistence: Register IDs stored in localStorage, auto-recovered on page load via get-or-create pattern
- Transaction Modifications: Void (marks existing transaction) and refund (creates inverse transaction with negated amounts) operations with reason tracking
- Payment Status Tracking: Transaction status auto-updates based on payment coverage (payments ≥ total → Completed)
Compliance Infrastructure
- Customer Verification Types: Age verification, medical card, ID scan, visual inspection, patient registration, purchase limit check
- Compliance Records: Per-transaction compliance tracking with verification status, state reporting status, and reference IDs
- Reporting Stubs: Compliance and daily financial report endpoints defined with types — implementation pending
Technology Stack
Frontend
| Concern | Technology |
|---|---|
| Language | PureScript — strongly-typed FP compiling to JavaScript |
| UI | Deku — declarative, hooks-based rendering with Nut as the renderable type |
| State | FRP.Poll — reactive streams with create/push for mutable cells |
| Routing | Routing.Duplex + Routing.Hash — hash-based client-side routing |
| HTTP | purescript-fetch with Yoga.JSON for serialization |
| GraphQL | purescript-graphql-client with AffjaxWebClient — inventory queries via /graphql/inventory |
| Money | Data.Finance.Money — Discrete USD (integer cents) with formatting |
| Async | Effect.Aff with run helper, parSequence_, killFiber for route-driven loading |
| Parallelism | Control.Parallel — concurrent data fetching within a single route |
Backend
| Concern | Technology |
|---|---|
| Language | Haskell |
| API | Servant — type-level REST API definitions |
| GraphQL | morpheus-graphql — inventory-scoped GraphQL resolver at /graphql/inventory |
| Database | postgresql-simple with sql quasiquoter, resource-pool for connection management |
| Server | Warp + warp-tls — HTTPS via TLS 1.2+ with mkcert certs in development |
| JSON | Aeson (derived + manual instances) |
| Auth | Dev-mode X-User-Id header lookup with role-based capabilities |
Infrastructure
| Concern | Technology |
|---|---|
| Database | PostgreSQL with reservation-based inventory, cascading deletes, parameterized queries |
| Dev Environment | Nix flakes — reproducible builds, per-machine dev shells |
| Secrets | sops + age — encrypted secrets/cheeblr.yaml, key derived from SSH ed25519 key |
| TLS | mkcert for local dev certs; warp-tls for HTTPS on the backend; Vite HTTPS config |
| Build (Haskell) | Cabal via haskell.nix / CHaP |
| Build (PureScript) | Spago |
| Testing | Haskell unit + integration tests; 484 PureScript tests; ephemeral-PostgreSQL integration harness |
Getting Started
Prerequisites
- Nix with flakes enabled
Development Setup
git clone https://github.com/harryprayiv/cheeblr.git
cd cheeblr
nix develop
# First-time: set up secrets and TLS
sops-init-key # derive age key from ~/.ssh/id_ed25519
sops-bootstrap # create secrets/cheeblr.yaml with a random DB password
tls-setup # generate mkcert dev certs for localhost
tls-sops-update # encrypt certs into secrets/cheeblr.yaml
sops-status # verify everything is wired up
# Start everything
pg-start
deploy # tmux session: backend (HTTPS :8080) + frontend (HTTPS :5173) + pg-stats
See Nix Development Environment for the full command reference, individual service scripts (backend-start, frontend-start, etc.), and the test suite.
API Overview
Session
| Method | Endpoint | Description |
|---|---|---|
| GET | /session | Current user capabilities (separated from inventory payload) |
Inventory
| Method | Endpoint | Description |
|---|---|---|
| GET | /inventory | All items with available quantities |
| POST | /inventory | Create item (Manager+) |
| PUT | /inventory | Update item (Cashier+) |
| DELETE | /inventory/:sku | Delete item (Manager+) |
| GET | /inventory/available/:sku | Real-time availability (total, reserved, actual) |
| POST | /inventory/reserve | Reserve inventory for a transaction |
| DELETE | /inventory/release/:id | Release a reservation |
| POST | /graphql/inventory | GraphQL endpoint — inventory queries (read-only) |
Transactions
| Method | Endpoint | Description |
|---|---|---|
| GET | /transaction | List all transactions |
| GET | /transaction/:id | Get transaction with items and payments |
| POST | /transaction | Create transaction |
| PUT | /transaction/:id | Update transaction |
| POST | /transaction/void/:id | Void with reason |
| POST | /transaction/refund/:id | Create inverse refund transaction |
| POST | /transaction/item | Add item (checks availability, creates reservation) |
| DELETE | /transaction/item/:id | Remove item (releases reservation) |
| POST | /transaction/payment | Add payment |
| DELETE | /transaction/payment/:id | Remove payment |
| POST | /transaction/finalize/:id | Finalize (commits inventory, completes reservations) |
| POST | /transaction/clear/:id | Clear all items/payments, release reservations |
Registers
| Method | Endpoint | Description |
|---|---|---|
| GET | /register | List registers |
| GET | /register/:id | Get register |
| POST | /register | Create register |
| POST | /register/open/:id | Open with starting cash |
| POST | /register/close/:id | Close with counted cash, returns variance |
Architecture
Frontend Architecture
The frontend follows a centralized async loading pattern:
Main.pursowns all async data fetching, route matching, and fiber lifecycle management- Pages are pure renderers:
Poll Status → Nut— no side effects, nolaunchAff_, noPoll.create - Route changes cancel in-flight loading via
killFiberon the previous fiber parSequence_runs multiple loaders in parallel per route- Status ADTs per page (
Loading | Ready data | Error msg | Degraded partialData) provide type-safe loading states pure Loading <|> pollensures pages always start with a loading stateTxPageDegradedallows the transaction
