In production since June 2026 · Four-location Missouri dispensary
Procurement intelligence for cannabis retail
Every buying decision a dispensary makes, backed by its own numbers.
ProCURE turns the exports a dispensary already pulls from its POS into a working buyer's desk: what to reorder, what this week's cash can cover, what is going stale, which vendors raised prices, and who owes the store a credit. This site walks through every page, what it does, the math behind it, and how the pages feed each other.
working pages across buying, inventory, and vendors
14 wks
from first commit to full production
184
reviewed and merged pull requests
103
database migrations, applied and verified
422
automated tests guarding the buying math
4
store locations running on it today
The buyer's job
Six questions a cannabis buyer answers every week. ProCURE answers each one with data.
A dispensary buyer juggles hundreds of SKUs across multiple stores, products that expire, vendors with minimums and ordering cycles, and a tax code (IRS Section 280E) that blocks most deductions and keeps cash tight. The POS records what sold. It does not tell the buyer what to do next. Each page in ProCURE exists to answer one of these questions.
01
What do we need to reorder, and how much?
Velocity from the trailing four weeks, vendor lead times, ordering cycles, and case packs turn into a suggested quantity for every product at every store.
02
What can we actually afford this week?
A weekly cash budget ranks every candidate by urgency, margin, and velocity, then draws the line where the money runs out.
03
What is sitting still or about to expire?
Idle products escalate by days without a sale, and a forward projection shows which lots will expire before they sell, in dollars.
04
Which vendors are raising prices on us?
Every lot's unit cost is tracked over time, so a vendor's quiet price increase shows up as a trend line instead of a surprise at month end.
05
Which brands earn their shelf space?
Margin, months of inventory, and sell-through roll up into an A to F grade per brand, with sub-brands rolled into their parent company.
06
Who owes us money for the discounts we ran?
Every promotion is classified as store-funded or vendor-funded, and vendor-funded discounts become a co-op credit ledger that can be applied to the next PO.
How the data flows
From a POS export to a signed purchase order.
ProCURE runs on the reports a dispensary already exports from Dutchie, its point-of-sale system. No integration project is needed to start. Everything downstream is built on two foundations: a sales timeline that accumulates, and an inventory balance that is replaced with every upload.
01 · Export
Three POS reports
Inventory, sales, and an optional discount detail report, pulled per store from Dutchie on whatever cadence the buyer works.
02 · Upload
Checked, cleaned, committed
Columns verified before anything is replaced
Shopper personal data stripped on arrival
Whole upload commits or none of it does
03 · Identity
One product, one record
Sales and inventory are matched by Metrc package tag first, then SKU, then normalized name, so renamed and re-tagged products stay one product.
04 · Analyze
Fifteen working pages
Velocity, margin, days of supply, expiry risk, cost trends, and discount funding, filterable by store and date range on every page.
05 · Act
A vendor-ready PO
Reorder and Open-to-Buy feed a buy session of draft POs per vendor and store, exported as PDFs with credits applied.
Why two foundations. Sales are dated events, so they stack up into a history that powers velocity, trends, and projections. Inventory is a snapshot of what is on the shelf right now, so each upload replaces the last one. Timelines are always built from sales and balances are always read from inventory. Keeping those two apart is what lets every page agree with every other page.
Page by page
Every page, what it decides, and how it connects.
Pick a page on the left, or switch to the full list to read them all in order. Each page links to the pages it draws from and the pages it feeds, and to the same screen in the interactive demo.
OverviewLive
Home
A command center that opens on what needs attention today, shaped to the person looking at it. An owner sees cash, margin, and every store side by side. A floor employee sees stock and tasks with no dollar figures at all.
The decision it supports
Where should I spend my first ten minutes? The alerts rail ranks what has gone wrong since the last upload, and each widget is a doorway into the page that fixes it.
What is on the page
Needs attention rail: urgent reorders, dead stock, cost increases
Sales performance, inventory value, brand leaderboard, top movers
Dead stock, cost creep, and open-to-buy summaries
Multi-location rollup and a quick stock lookup
Credits owed by vendors
How it works
Every widget passes two gates before it renders. The first asks whether its data source is live for this company. The second asks whether this person's role is allowed to see it. Six roles are defined (owner, manager, accounting, staff, viewer, and a platform admin), and the same checks run again on the server, so hiding a button is never the only protection.
A brand-new company with no data does not see an empty grid. It gets a guided first run with two paths: upload the reports yourself, or drop raw files into a private intake folder and have them loaded for you. The fork disappears after the first upload.
The working list for every buy. Each product at each store gets a suggested order quantity, and anything on track to sell out before a delivery could land is flagged urgent and pushed to the top.
The decision it supports
What should go on this week's order, and in what quantity? The buyer reviews, adjusts, and adds lines to the buy session. Nothing is ordered automatically.
What is on the page
On hand, velocity, days of supply, and suggested quantity per product
Urgent tag when stock is already below the reorder point
A review dialog to override a quantity once or save a rule
Badges showing where each lead time came from
Excluded items (samples, promos) kept visible behind a toggle
How it works
velocity = units sold in the trailing 28 days ÷ 28
reorder point = velocity × lead time ← urgent when on hand is below thissuggested = velocity × horizon × (1 + cushion) − on hand
rounded up to a whole case pack
Lead time and safety cushion resolve from the most specific setting available: a product override, then a brand override, then the vendor's agreed lead time, then the company default (14 days and a 25% cushion out of the box). For a vendor on a fixed ordering cycle, the horizon becomes the cycle plus the lead time, because the shelf has to last until the next scheduled order lands. Samples, promotional items, and stash boxes match an exclusion rule and never count toward ordering math. They are tagged rather than deleted so the buyer can still see them.
Turns the reorder list into a buy the business can pay for. The buyer enters this week's cash budget, and ProCURE ranks every candidate and allocates the money from the top down, showing exactly where the cut line falls.
The decision it supports
With a fixed amount of cash, which lines make this week's order and which wait? Under 280E, inventory purchases compete with payroll and rent, so the reorder list is a wish list until it meets a budget.
What is on the page
A saved weekly budget and a priority-ranked candidate list
Each line marked in budget, skipped, or urgent but over budget
Score breakdown: urgency, margin, velocity
Spend split by vendor for the in-budget lines
One click to add the in-budget lines to the buy session
How it works
score = 0.4 × urgency + 0.4 × margin + 0.2 × velocity
urgency 1.0 below reorder point · 0.8 under 7 days · 0.5 under 14 days · fades to 0 by 60 days
margin (price − cost) ÷ price, scaled against the best margin on the list
labels Critical ≥ 0.7 · High ≥ 0.5 · Medium ≥ 0.3 · Low
Allocation runs in two passes. The first walks the ranked list and takes every line that still fits. The second goes back for smaller lines that fit the leftover money. A line with an unknown cost is never treated as free: it cannot quietly consume zero dollars and slip under the cap. Urgent items that do not fit are called out separately, so a stockout risk is never hidden behind the word "skipped."
A persistent buy session. Lines added from Reorder or Open-to-Buy collect into draft purchase orders, one per vendor per store, and stay there between visits until the buyer exports them.
The decision it supports
Is each vendor's order ready to send? Does it clear the vendor's minimum, come in whole cases, and reflect current costs? Should a credit the vendor owes come off this invoice?
What is on the page
Draft POs grouped by store, then vendor
Minimum order badge: "add $X or hold" when under the vendor's floor
Case-pack notes and requested net-new lines
A warning when a newer upload changed costs under the draft
Co-op credits applied against the PO total
Export one vendor as a PDF, or every PO as a zip organized by store and vendor
How it works
The cart is not one shopping basket. It is many draft POs at once, keyed by vendor and location, saved in the database so a buyer can build the order over several days. Each line remembers which upload its cost came from. If a later upload replaces inventory, the line is flagged as possibly stale rather than silently repriced.
Vendor minimums are usually a dollar floor (for example $2,500), sometimes a unit floor. A missing floor means "not set," never a $0 floor that everything clears. Applying a vendor's co-op credit lowers the net due on that PO and moves the credit from owed to applied, and later to settled, so the same dollar can never be claimed twice.
Tracks what the store actually paid for every lot of every product over time, and surfaces the products whose cost has moved the most. A vendor's gradual price increase becomes a visible trend instead of a margin leak nobody notices.
The decision it supports
Which vendor conversations should happen this week? Rising costs are a reason to negotiate or switch. Falling costs are a buying opportunity.
What is on the page
One row per product with a sparkline of unit cost by lot
Headline drift: newest lot versus oldest tracked lot
Sorted by largest movement, increases and decreases
Per-store breakdown
How it works
Inventory is replaced with every upload, so comparing inventory snapshots would erase history. Instead, cost is read from the sales side: every sale records the package it came from and that package's unit cost, which builds a durable cost history per lot. Lots are grouped by product, ordered newest first, and any product whose lots all cost the same is dropped as noise.
Samples and promotional packages (often costed at a penny) are excluded, and costs under $1 are ignored, so a free sample never reads as a 99% price drop. The page and the Home widget share one calculation, so they can never disagree.
The master view of everything in stock. Batches and rooms collapse into one line per product, with cost, price, margin, velocity, days on hand, and expiration risk side by side.
The decision it supports
How is any single product doing? It is the lookup table behind every other page: when a number elsewhere looks odd, this is where the buyer checks it.
What is on the page
SKU, brand, category, on hand, inventory value
Unit cost, current price, margin
Velocity and days on hand
Expires in and $ at risk from the expiration projection
Live search, store and date filters
How it works
unit cost = quantity-weighted average across lots with a known cost
margin = (price − cost) ÷ price ← blank, never 100%, when cost is unknowndays on hand = on hand ÷ velocity (trailing 28 days)
A product can sit across several batches bought at different costs, so cost is weighted by quantity. When no lot has a known cost, the margin shows a dash instead of a perfect score. A missing number that reads as a star product is worse than no number at all.
A pick list for the back room. It finds products that are out or nearly out on the sales floor while sellable stock waits in the vault, and names the exact package to move first.
The decision it supports
What should staff pull from backstock right now? Lost sales from an empty shelf are invisible in the POS, because a sale that never happened leaves no record.
What is on the page
Products marked OUT or LOW on the floor
Floor units, backstock units, and floor days of supply
The recommended package to move, with its expiration date
A warning banner if no floor stock is detected at all
How it works
A product is flagged when its floor stock covers less than one day of sales while sellable backstock exists. The recommended package follows first-expired, first-out: the backstock package that expires soonest goes first, with the earliest received date breaking ties. Products that are out sort above products that are low, then by fewest floor days and soonest expiration.
Every store names its rooms differently, so each company maps its own rooms to "floor" or "backstock" instead of relying on a room literally named "Sales Floor." Samples and promotional packages are never put on the pick list.
Finds the products that stopped selling and shows how much cash is tied up in each one, with priority rising the longer a product sits.
The decision it supports
What should be discounted, moved to another store, or returned before it becomes a write-off? And which products should never be reordered?
What is on the page
Days since last sale and capital tied up per product
Priority bands from watch to high
An expiring soon escalation for dead stock with a clock on it
Dismiss with a reason, so reviewed items stop resurfacing
How it works
Band
Rule
Watch
21 to 29 days without a sale
Low
30 to 59 days
Medium
60 to 89 days
High
90+ days, or in stock and never sold
Expiring
Any dead product with a lot expiring within 90 days
The thresholds came from the operator who uses the page. Last-sale dates are computed in the database per product, so the page never pulls raw transactions and never hits a row limit. A product renamed in the POS still lands on its original record because sales are matched by package tag first.
Projects today's stock forward at the current selling pace and shows which lots will reach their expiration date before they sell, and how many dollars that represents.
The decision it supports
What needs a promotion or a transfer now, while there is still time to sell it? Expired cannabis cannot legally be sold, so every unsold unit at expiration is a full loss.
What is on the page
Lots projected to expire unsold within 90 days
Projected sell-out date versus expiration date
Units and dollars at risk per lot and in total
A plain-language note on what the projection assumes
How it works
for each product, walk its lots soonest-expiring first:
days to sell through = (units ahead in line + this lot) ÷ velocity
at risk if that is later than the lot's expiration
$ at risk = projected unsold units × unit cost
Velocity is measured per product, but expiration belongs to each lot. The projection bridges the two by burning the product's velocity down through its lots in the same first-expired, first-out order Floor Replenishment recommends. The page states that assumption openly instead of presenting a projection as fact. In live data, 88% of lots carry an expiration date and all of them carry a cost, so the projection rests on real inputs.
Measures what share of available stock actually sold in each reporting period, and whether that rate is improving or slipping compared with the period before.
The decision it supports
Is this product earning its space, and is demand rising or fading? It catches a slowdown before the product becomes dead stock.
What is on the page
Sell-through rate per product with a rate bar
Trend arrow versus the previous period
Units sold and on hand
Filters by brand, category, format, and store
How it works
sell-through = units sold ÷ (units sold + units on hand)
trend up or down when the change exceeds 2 percentage points, otherwise flat
The previous period's rate uses the inventory that was actually on hand at the end of that period, archived when the next upload replaced it. An earlier shortcut reused today's inventory for both periods, which made the arrow track units sold rather than true sell-through. Where no archived snapshot exists, the page shows no trend instead of a misleading one.
The vendor book behind every purchase order. Each vendor has a detail page holding the agreement terms that drive the buying math: lead time, ordering cycle, minimum order, and case sizes.
The decision it supports
Are this vendor's terms recorded correctly? Every term entered here changes what Reorder suggests and what the PO page enforces, so keeping it accurate keeps every buy accurate.
What is on the page
Contact, PO email, payment terms, lead time
Agreement: dollar and unit minimums, ordering cycle, case size per product
Which brands each vendor supplies
Co-op credits applied and settled
Merge for duplicate vendors spelled two ways
How it works
Vendors are created automatically from the distributor column in each upload, so the book fills itself in. Because the same distributor often appears under slightly different spellings, vendors can be merged: the duplicate becomes an alias, and future uploads route to the surviving vendor.
Setting an ordering cycle on a vendor switches its products from "order when low" to "order enough to last until the next scheduled order." Setting a case size makes Reorder round up to whole cases, so a need of 33 units on a 10-pack becomes 40, never 30.
A leaderboard of every brand on the shelf, with full financials and a letter grade that combines profitability with how fast the brand's inventory turns.
The decision it supports
Which brands deserve more shelf space and more of the budget, and which should be cut back? It is the starting point for line reviews and vendor negotiations.
What is on the page
Inventory value, net sales, COGS, gross profit in dollars and percent
Months of inventory on hand
A to F grade per brand
Expand any brand into its formats (flower, vape, edible, and so on)
Room filter and a self-serve brand mapping screen
How it works
Grade
Gross margin
Months of inventory
A
50% or more
2 or fewer
B
40% or more
3 or fewer
C
30% or more
4 or fewer
D
20% or more, or
6 or fewer
F
Everything else
Sales are normalized to a monthly rate, so a weekly report and a monthly report grade the same way. Brands roll up to their parent company: a company that ships under five names reports as one, and spelling variants collapse to a single brand. A starter mapping is seeded per state, and each company can override it.
Every discount given at the register, rolled up into the promotions that caused it, classified by who pays for it, and turned into a ledger of credits vendors owe the store.
The decision it supports
How much margin went to discounts, which promotions are worth repeating, and which vendors owe a credit for promotions they agreed to fund? Unclaimed vendor credits are money the store has already earned and never collected.
Five tabs
Overview: discounts as a share of gross sales, store versus vendor funding, daily trend, split by store and by customer group
Promotions: each promotion rolled up, with funding set inline and filters by category and program
Vendor Summary: discount dollars by vendor
Overrides: promotions keyed in by hand at the register, a signal that a discount setup broke
Credits: co-op credits owed per vendor, with memo export as PDF, spreadsheet, or zip
How it works
An optional third upload, the POS discount detail report, provides promotion-level data. Product categories come from matching each line's package tag back to inventory. Customer names and other personal fields in that report are removed by the parser before anything is saved.
Each promotion is classified as store-funded or vendor-funded. ProCURE suggests a classification from the discount code, but a suggestion is never applied on its own: a person confirms it, and an unclassified promotion stays visibly unclassified. Vendor-funded discounts become credits owed, grouped by how the vendor reimburses. Each vendor gets a credit memo with an overview by reimbursement type and a line per promotion. It carries no transaction, staff, or customer detail. A credit can also be applied directly to that vendor's next purchase order.
Where every number in ProCURE begins. The buyer drops in the POS exports, and the upload checks them, cleans them, and commits them in one step, or refuses them before any data is touched.
The decision it supports
Is this file safe to load? A mislabeled export that silently wiped good inventory data would poison every page at once, so the upload is built to fail loudly instead.
What is on the page
Drag-and-drop for inventory, sales, and discount detail files
A column report for each file: recognized, missing, ignored
A hard stop if a required column is missing or renamed
Package tag coverage, and a warning when cost columns are absent
Store confirmation on the first upload for each location
A concierge option: drop raw files and have them loaded for you
How it works
Files go straight from the browser to private storage through short-lived signed links, which raises the limit to 25 MB per file instead of the hosting platform's 4.5 MB per request. The server then parses them and stages the results. A single database transaction swaps the new inventory in and appends the new sales, so a failure halfway through leaves the previous data exactly as it was. Raw files are deleted after processing.
Columns that identify shoppers are dropped and reported on arrival, so consumer personal data is never stored. A new store can load two or three months of sales history one month at a time, which gives it near-complete analytics on its first day.
Self-serve account management: invite teammates, assign roles, change passwords, and keep a record of who agreed to which version of the terms.
The decision it supports
Who should see what? Cost and margin data is sensitive, so a budtender and an owner need very different views of the same company.
What is on the page
Invite by email, assign owner, manager, staff, or viewer
Remove a teammate without erasing their history
Password change and reset
Terms, Privacy, and DPA acceptance recorded per user and version
How it works
Read access is broad and write access is narrow: only owners and managers can upload data, change buying settings, or edit vendors, and every one of those actions is checked on the server against the person's role and company. Sessions end after 30 minutes of inactivity with a one-minute warning, a password change signs out other sessions, and a removed teammate loses access on their next request.
Invited users accept the Terms of Service and Privacy Policy before their first login, and owners also accept the Data Processing Agreement. Each acceptance is stored with the document version, so a material change to the terms prompts everyone to accept again.
Real retail data is messy: products get renamed, tags rotate, reports change shape, and two data feeds can disagree. These are the outcomes of the work that went into making the numbers trustworthy.
100%
of sales counted, at any volume
Every report is aggregated inside the database, so pages never pull raw transactions and never run into the 10,000-row response limit that would otherwise drop sales without an error.
69%
fewer unmatched sales
Matching by package tag, then SKU, then name reduced the sales that could not be tied to a product from 2,687 groups to 826. The rest was audited and confirmed as permanent sold-through history.
4.6×
faster on the heaviest pages
A precomputed sales rollup cut the core velocity query from about 555 ms to 121 ms per store, and a separate rewrite took a 42-second data-quality panel down to 1.5 seconds.
0
half-finished uploads
Uploads commit all at once or not at all, and files with renamed or missing key columns are refused before they can overwrite good data.
$0.00
reconciliation difference on discounts
When two discount feeds were found overlapping, the duplicate counting was removed and the lost promotion detail was restored from backup. Totals reconciled to the penny across all four stores.
25 MB
per file, up from 4.5 MB
Direct-to-storage uploads removed the hosting platform's request cap, so a large store's monthly export loads in one pass.
31
server actions audited for tenant isolation
Every data-changing action routes through one guard that checks role and company, and an automated two-company test fails the build if data could cross between tenants.
Nightly
encrypted backups, restore tested
AES-256 encrypted database backups run every night with an offsite copy, and a full restore was performed and verified table by table.
Day 1
analytics for a new store
Loading recent months of sales history gives a newly onboarded store working velocity, trends, and reorder suggestions on its first day instead of after weeks of uploads.
How it was built
Rules the product follows on every page.
Suggestions, not auto-orders
The tool makes the case and a person makes the call. For a product that is regulated, perishable, and paid for in cash, every purchase order is reviewed and exported by a buyer.
Unknown is never zero
A missing cost shows as a dash, never $0.00 or a 100% margin. A blank number that looks like a great number leads to bad buys.
Flag, don't delete
Excluded, recalled, on-hold, and non-cannabis items are kept out of the math but stay visible, so nothing disappears without the buyer knowing why.
Every number reconciles
Pages share one calculation for each metric, and data fixes are verified against a restored copy of production before they ship.
The stack
Application
Next.js + TypeScript
Server-rendered pages, about 26,000 lines of TypeScript, role checks on every server action.
Database
Supabase Postgres
Row-level security, analytics in SQL functions, 103 versioned migrations.
Hosting
Vercel + Cloudflare
Production hosting, DNS, enforced security headers, offsite backup storage.
Quality
Vitest + CI
422 tests on the buying math, type checks, lint, and dependency scanning on every change.
Monitoring
Sentry + uptime checks
Errors captured with personal data scrubbed, plus outside-in availability alerts.
Email
Resend
Branded invite and password-reset emails from a verified domain.
Inventory, sales, and discount detail CSVs, with Metrc package tags as the shared key.
Where ProCURE stands
Built, shipped, used, and closed for clear reasons.
ProCURE was built from June to September 2026 and runs in production for a four-location Missouri dispensary, where the buyer uses it to plan orders, track credits, and catch problems before they cost money. Its features were shaped day to day by the operators using it.
In September 2026, a market review found that the major point-of-sale platforms now include reorder suggestions and inventory analytics in their core product at no extra charge. A standalone paid tool would need a feature they do not offer, and the strongest candidates (receiving reconciliation against state manifests and neutral vendor scorecards) were still on the roadmap without a paying customer to validate them. ProCURE LLC closed the venture rather than keep spending on an unproven bet.
The platform still runs for its original store. This site documents what it does and why it was built the way it was.
JUN 2026
First commit. Upload, catalog, sell-through, brand grades, dead stock, reorder, floor replenishment, Open-to-Buy, and Cost Creep in production within the month.
JUL 2026
Role-based Home, the product identity resolver, Expiration Burn-Down, vendor detail pages, and the persistent buy session.
AUG 2026
Vendor ordering model and PO export, the discount tracker rebuild with co-op credits, atomic uploads, security hardening, and the consent ledger.
SEP 2026
Onboarding for new stores, performance work, a discount data fix reconciled to the penny, then the market review and the decision to close.
Where it was going
The V2 and V3 roadmap at the time the venture closed.
Each version was sequenced by dependency: what the data could support, and what the next version would need. The V2 architecture was fully planned, with read-only state compliance access and encrypted credential storage, and was waiting on Missouri Metrc API registration.
V1.5 · remaining
The cash-aware buyer
Buildable on existing data. Most of V1.5 shipped; these two items were next.
PO approval gateA manager drafts, an owner approves spend over a set amount. Large multi-store orders justify a second signature.
Transfer suggestions between storesOverstock at one store and a stockout at another are already visible in the data. Shown as advice that reminds the buyer to verify state transfer rules, never as an automatic action.
V2
Did it arrive, did it pass
Gated on live Metrc and Dutchie connections.
Live Metrc and Dutchie syncReplace uploads with scheduled syncs, staged and then promoted into the same data model the pages already read.
Receiving reconciliationMatch each PO to the Metrc transfer manifest and the invoice, catching short shipments and wrong costs at the dock and keeping discrepancies under regulatory thresholds.
COA and lab-test captureRecord pass or fail and batch at receiving, tied to the product.
280E-aware cost captureSeparate deductible product cost from non-deductible spend at the moment of receiving.
Pushed alertsStockout risk, expiring stock, and floor outages sent the moment they happen.
V3
Who earns the reorder
Needs accumulated history and accounting sync.
Vendor scorecards, phase 2Add lab-test failure rate and recall history, the signals that matter only in cannabis.
Single-source riskFlag products and brands with no backup supplier.
Event-aware forecastingAccount for 4/20, holidays, and vendor promotions in velocity.
Accounting syncQuickBooks and Xero, carrying the 280E cost separation into the books.
Scan-to-sell intakeScan the Metrc tag and manufacturer barcode once at receiving on a phone, and create a sellable product in the POS automatically.
Multi-state platformAdditional state compliance systems and tools to manage many tenants.
Deliberately not on the roadmap
Competitive bidding and e-auctions. Wrong fit for licensed distributors.
Contract lifecycle management. More process than this buyer needs.
Enterprise supplier-risk scoring. Replaced by the narrow single-source flag.
Procurement punchout plumbing. Enterprise weight with no payoff here.
A wholesale marketplace. ProCURE stays on the buyer's side of the order.
Fully automatic purchase orders. A person approves every buy.
See it working.
The interactive demo runs every page on a sample four-store company, so you can click through the reorder list, move the budget, build a PO, and open the vendor credits yourself.