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.
- 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 and configuration of the Nix-based development environment
- Dependencies β Project dependency listing
- To Do list β Planned features and optimizations
- Security Recommendations β Planned security and authentication upgrades
- 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
- 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
parallel/sequentialpattern - 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
- 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)
- 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
| 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 |
| 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 |
| Concern | Technology |
|---|---|
| Language | Haskell |
| API | Servant β type-level REST API definitions |
| Database | postgresql-simple with sql quasiquoter, resource-pool for connection management |
| Server | Warp |
| JSON | Aeson (derived + manual instances) |
| Auth | Dev-mode X-User-Id header lookup with role-based capabilities |
| Concern | Technology |
|---|---|
| Database | PostgreSQL with reservation-based inventory, cascading deletes, parameterized queries |
| Dev Environment | Nix flakes for reproducible builds |
| Build (Haskell) | Cabal |
| Build (PureScript) | Spago |
| Database Service | NixOS systemd integration |
- Nix package manager with flakes enabled
git clone https://github.com/harryprayiv/cheeblr.git
cd cheeblr
nix develop
deployThis launches the full development stack: PostgreSQL service, backend API server (port 8080), and frontend dev server (port 5174).
| Method | Endpoint | Description |
|---|---|---|
| GET | /inventory |
All items with available quantities and user capabilities |
| 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 |
| 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 |
| 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 |
The frontend follows a centralized async loading pattern inspired by purescript-deku-realworld:
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) provide type-safe loading states pure Loading <|> pollensures pages always start with a loading state
Main.purs (orchestration)
βββ Route matcher β killFiber prev β parSequence_ loaders β build page Nut
βββ Loading functions: loadInventoryStatus, loadEditItem, loadDeleteItem, loadTxPageData
βββ Callback-to-Aff wrappers (makeAff) for RegisterService integration
Pages/ (pure renderers)
βββ LiveView: Poll InventoryLoadStatus β Nut
βββ EditItem: Poll EditItemStatus β Nut
βββ DeleteItem: Poll DeleteItemStatus β Nut
βββ CreateTransaction: Poll TxPageStatus β Nut (parallel: inventory + register + tx)
βββ CreateItem: UserId β String β Nut (no async needed)
βββ TransactionHistory: Nut (placeholder)
App.hs (bootstrap, CORS, middleware)
βββ Server.hs (inventory handlers with capability checks)
βββ Server/Transaction.hs (POS: transactions, registers, ledger, compliance)
βββ Auth/Simple.hs (dev auth: X-User-Id β role β capabilities)
βββ DB/Database.hs (inventory CRUD, connection pooling)
βββ DB/Transaction.hs (transactions, reservations, registers, payments)
βββ Types/ (domain models with Aeson + postgresql-simple instances)
- Item Selection: User adds item β backend checks
quantity - reservedβ creates reservation β returns item - Cart Management: Items tracked via transaction items table β server recalculates totals on each change
- Payment Processing: Payments added β server checks if payments β₯ total β auto-updates status
- Finalization:
menu_items.quantitydecremented β reservations markedCompletedβ transactionCOMPLETED - Cancellation:
POST /transaction/clear/:idβ reservationsReleasedβ items/payments deleted β totals zeroed
| Table | Purpose |
|---|---|
menu_items |
Product catalog with stock quantities |
strain_lineage |
Cannabis-specific attributes (THC, terpenes, lineage) β FK to menu_items |
transaction |
Transaction records with status, totals, void/refund tracking |
transaction_item |
Line items β FK to transaction with CASCADE |
transaction_tax |
Per-item tax records β FK to transaction_item with CASCADE |
discount |
Discounts on items or transactions β FK with CASCADE |
payment_transaction |
Payment records β FK to transaction with CASCADE |
inventory_reservation |
Reservation tracking (Reserved β Completed/Released) |
register |
Cash register state and history |
- Parameterized queries throughout β no string interpolation in SQL
- Type safety across the full stack β shared domain types between PureScript and Haskell
- Role-based capabilities β 15 granular permissions mapped to 4 roles, enforced on inventory writes
- Input validation β frontend (ValidationRule combinators) and backend (type-level constraints via Servant)
- Audit trail β transactions track void/refund reasons, reference transactions, and modification timestamps
Current limitation: Authentication is dev-mode only (X-User-Id header with fixed users). See Security Recommendations for the planned upgrade path.
- β Full inventory CRUD with strain lineage
- β Inventory reservation system (reserve on cart add, release on remove, commit on finalize)
- β Complete transaction lifecycle (create β items β payments β finalize/void/refund/clear)
- β Multiple payment methods with change calculation
- β Cash register open/close with variance tracking
- β Tax and discount record management
- β Role-based capability system (4 roles, 15 capabilities)
- β
Dev auth with
X-User-Idheader and user switcher widget - β Centralized async loading with fiber cancellation on route change
- β Parallel data loading for POS page (inventory + register + transaction)
- β
InventoryResponseincludes user capabilities for frontend UI gating
- π Daily financial reporting (endpoints exist, implementation pending)
- π Compliance verification system (types and stubs defined)
- π Real authentication (JWT or session-based, replacing dev
X-User-Idheader) - π Capability enforcement on transaction/register endpoints
- π Inventory reservation expiry (cleanup of abandoned reservations)
- π Transaction history page (currently a placeholder)
- π Advanced reporting and analytics
- π Multi-location support
- π Third-party integrations (Metrc, Leafly)
- π Auto-refresh/polling for live inventory view
This project is licensed under the MIT License β see the LICENSE file for details.
Contributions are welcome. Please feel free to submit a Pull Request.