Set up needed

The Supabase credentials haven't been configured yet. Open this file and fill in two values near the top of the script:

const SUPABASE_URL = "https://YOUR-PROJECT.supabase.co";
const SUPABASE_ANON_KEY = "your-publishable-or-anon-key";

Then re-deploy the file. The full setup guide lives in SETUP-cloud-inventory.md next to this file.

sixforty

HQ

Sign in to continue.

Access is invite-only. Ask Leo to add you.
sixforty HQ
Connecting...
How CSV import works
The Import CSV button auto-detects whether you're dropping a Shopify products export or an orders export and routes it to the right place. For products: reads Handle, Title, Option1 Value, Variant SKU, Variant Inventory Qty, Variant Price. Existing SKUs update quantity and price. Cost is not imported: it is derived from purchase allocations (weighted average per product). Manual fields (reorder, supplier, lead days) are preserved. For orders: reads Name, Created at, Lineitem sku, Lineitem quantity, Lineitem price, Discount Code. Each line item becomes a sale. Duplicate order+SKU combos are skipped automatically. Discount codes feed the Affiliates tab.

Stock value, top 12 SKUs (at cost)

Inventory

No inventory yet

Click Import CSV and drop a Shopify products export, or Add product to enter one by hand.

Recently deleted (0)
SKUProductDeletedActions

No recently deleted rows.

Sales performance

Weekly trend

Top sellers

Ranked by units sold
#ProductUnitsRevenue

Velocity

Units per week, weeks of cover
#ProductUnits/wkOn handCoverPace

Margin leaders

Most profit earned in this period
#ProductUnitsMargin/unitTotal marginMargin %

Customer economics

ARPU, CAC, LTV and repeat rate

Top customers

Ranked by revenue
#CustomerOrdersRevenueAOV

Geography

Where the orders ship
Country / CityCustomersOrdersRevenue

No sales yet

Drop a Shopify orders CSV onto the Import CSV button, or load demo data to see the analytics in action.

How transactions work
Every transaction is a double-entry journal. Money flows from one account (credited) into another (debited). Buying inventory: Debit Inventory, Credit Bank. Paying for marketing: Debit Marketing, Credit Bank. A refund reverses the direction. When you imported your old Monzo CSV the categories were mapped automatically. New CSV imports keep working the same way. To add accounts, run an UPDATE/INSERT in Supabase against public.accounts; everything in the dropdowns refreshes in real time.

Spend & reconciliation

Transactions

DatePayeeDebitCreditAmountSourceAllocated

No transactions yet

Click Import CSV and drop a Monzo export, or click Add transaction to add one by hand.

Financial statements

Fiscal year start (click to edit)
Used for YTD calculations. All balances are derived from the ledger; no manual opening balances needed.
How marketing CSV import works
Export campaign performance from Meta Ads Manager (or Google Ads) as a CSV, then click Import CSV. Sixforty auto-detects Meta exports by their column headers (Campaign name, Amount spent, Reporting starts, Reporting ends, Impressions, Reach, Clicks, CTR, CPC, CPM, Frequency, Results, Purchases conversion value). One CSV row becomes one campaign-period record. Re-imports for the same campaign and date range update in place. Blended ROAS below is computed against actual Shopify revenue in the same period, so you can compare Meta's reported attribution to what hit the till.

Marketing performance

Spend over time

Campaigns

Campaign Period Spend Impressions Reach Clicks CTR CPC CPM Conv. Conv. value Meta ROAS

No marketing data yet

Export a CSV from Meta Ads Manager and click Import CSV. The columns are auto-detected, so any standard campaign export will work.

How reconciliation works
Each panel below checks a specific consistency rule in your books at a chosen as-of date. Green means the numbers match, amber means there's a gap worth investigating. Bank balances are populated automatically from any native Monzo CSV you import (the Balance column); you can also enter a point by hand. Shopify snapshots come from periodic pulls via the Shopify MCP (or manual entry). Once you've set up the scheduled task, this whole tab runs hands-off.

Ledger sanity check

Assets must equal Liabilities + Equity

Bank reconciliation

Ledger Bank balance at end of month vs Monzo statement balance
Add or correct a Monzo balance point

Sales reconciliation

HQ sales for the month vs Shopify-reported revenue for the same month
Add or correct a Shopify snapshot

Inventory valuation (subledger to ledger)

Stock on hand at cost by product, tied to the Inventory (1200) ledger balance

Inventory units (HQ vs Shopify)

HQ-derived units (opening + received - sold - written off) per product vs Shopify on hand. Negative derived means receipts not yet allocated; large positive gap means off-Shopify sales or an overstated receipt.

Owner loan position

Outstanding balance owed to you by the business

Marketing reconciliation

Meta-reported ad spend vs ledger marketing transactions

Shipping cost recovery

Postage charged to customers vs courier cost in the ledger
How the Sales tab works
A two-way reconciliation: every sale should tie to a payment, and every payment to a sale. Order -> payment: each Shopify order shows how it was paid, a Stripe payment link, a direct bank credit, or a card sale settled inside a bundled payout (reconciled in aggregate, not per order). Payment -> order: "Stripe payments with no sale" and direct sales not yet allocated are payments still waiting on a sale to be entered. Each order also carries its units + cost of goods from the sales table; Shopify sales should be costed and not refunded. Note: this view reads what HQ has recorded. It cannot see a Shopify order's own total, so a comped order shows at list price here, confirm against Shopify if a value looks off.

Sales coverage

Every sale and whether its units, cost and revenue are captured correctly

Shopify sales (web + POS)

Grouped by order. Units and cost come from the sales table; revenue arrives via Stripe payouts in the ledger.

Direct sales (no Shopify order)

Bank-paid sales that predate POS. Revenue is the ledger transaction; units and cost need an "Add sale" allocation.

Stripe payments with no sale

Payment-link charges that don't yet match a Shopify order. Each is a sale to enter, the payment -> order half of the reconciliation.

Payment exceptions

Orders booked as sales but not marked paid in Shopify, and Shopify orders not yet imported into HQ. Each needs a call from you or George.
How the Affiliates tab works
Affiliates promote Sixforty with a unique discount code. When a customer uses the code at checkout, the order is credited to that affiliate (attribution is by code, captured from the Shopify orders CSV). Each row below is one code: net sales (what customers actually paid, after the discount, excluding refunds), the orders and units it drove, and the commission due at the rate you set. Set the commission rate per code; it saves straight away. Commission is a guide for what to pay monthly in arrears, book the payout to account 6700 (Affiliate commissions) in Transactions. Recommended default: 15% customer discount, 10% commission, stepping to 15% (Growth) and 20% (Hero) as an affiliate produces.

Affiliate summary

All-time totals across every discount code

By affiliate code

Net sales and commission due per code. Set the rate to match each affiliate's agreement.

Recent activity

WhenWhoActionTargetChanges

No activity yet

Every change you make to inventory or sales will appear here.

Backups

Save snapshot stores the current inventory and sales in a rollback table, useful before a stock count, a big price change, or a bulk edit. Export books downloads every table (the full ledger: transactions, allocations, splits, batches, plus sales, inventory and the reconciliation feeds) as CSVs in a zip; keep a copy somewhere outside Supabase. Supabase also takes daily automatic Postgres backups, so this is belt-and-braces.
How the Tasks tab works
Open actions that keep the books complete and reconciled. Work through these as they appear. The "Allocate" button jumps straight to the transaction in Transactions to fix it. Products needing a cost are resolved by allocating their purchase: cost is derived from allocations, not entered by hand. "Sales with no Shopify match" lists bank payments that don't tie to a Shopify order, either a recent sale to enter in Shopify POS, or an older off-Shopify sale to record via "Add sale". The match is on amount and date, so treat it as a review list, not gospel. Only categories with open items are shown; anything fully reconciled is hidden.
All clear. Every payment ties to a sale and every sale to a payment, nothing needs attention right now.

Bank transactions needing a category

Auto-imported from Monzo but the payee wasn't recognised, so booked to Uncategorised (9998). Categorise each one, and the rule learns nothing yet, so tell Leo if a payee recurs.

Sales with no Shopify match

Bank payments (excluding Stripe) with no matching Shopify order, enter in POS or allocate

Stripe payments not in Shopify

Stripe charges with no matching Shopify order, enter the sale in Shopify

Orders booked but not paid in Shopify

A sale is recorded in HQ but Shopify has not marked the order paid, confirm it was collected or fix the order

Shopify orders not imported to HQ

Orders that exist in Shopify but have no sale lines in HQ, import the orders CSV to bring them in

Sales with no payment recorded

A sale is booked but no payment is in the ledger yet (no Stripe charge, not card-at-checkout, no matching bank deposit). Often a recent cash/POS order whose deposit has not been imported.

Products needing a cost

Cost is £0, so sales of these post no COGS

Inventory purchases not fully allocated

Spend on Inventory (1200) not yet assigned to products

Purchases & returns not assigned to a batch

Assign each payment to a batch so FIFO can cost it. Receipt carries the units; extra payments go on the same batch as cost-only (quantity 0). Inventory returns (money back, "(inventory return)") attach to the batch the stock came from and reduce it.

Inventory stock gaps (Shopify vs HQ)

Every product where HQ's derived stock (opening + purchases, less sales and write-offs) differs from Shopify's on-hand at all. A positive diff means HQ shows more than Shopify (often gifts/samples that left unrecorded); a negative diff means Shopify shows more (often an unrecorded receipt or a count difference). Monitoring only, not counted as an action.

How the books flow

What happens at each stage, and the two steps you need to do by hand
You do this Automatic in HQ 1 · Inventory cost → COGS Pay supplier for stock Dr Inventory (1200) · Cr Bank (1010) Imported from Monzo AUTO Allocate the spend to product(s) Expand txn → pick product + batch, qty, £ Product needs a SKU + handle YOU Product cost updates FIFO: each batch keeps its own cost AUTO A sale books the cost Dr COGS (5000) · Cr Inventory (1200) Run Recost (FIFO) after changes AUTO 2 · Revenue → Inventory Money in from a sale Dr Bank (1010) · Cr Sales (4000) Stripe payout or direct transfer AUTO Record the sale in Shopify Web order, or enter it in POS "Add sale" only if never in Shopify YOU Stock drawn down + COGS Dr COGS (5000) · Cr Inventory (1200) AUTO Check the reconciliations Sales & units should tie (green) Revenue stays on the transaction YOU

The two jobs that are yours

1. Allocate inventory purchases to a batch. When money goes out to a supplier (Dr Inventory), open that transaction in Transactions, expand it, then under "Allocate" pick the product, choose or create a batch (one production run), and enter the quantity received and total spent. A second payment for the same batch (deposit/balance, shipping, duty) goes on that batch as a cost-only line (quantity 0), so it adds cost without inflating units. A supplier refund is recorded as a return against the batch it came from. Cost is then derived per batch using FIFO (oldest batch sold first) and is read-only, you never type a product's cost by hand. The product must already exist with a SKU and a handle.

2. Record the sale in Shopify. Every sale should go through Shopify, a web order, or one George enters in Shopify POS. The units and cost then come into HQ from the Shopify order. The "Add sale" allocation is only a fallback for older sales that were never put through Shopify (it records the units and cost against the bank payment). Use it for the historical backlog, not day-to-day; a sale should never be in both Shopify and an allocation.

The golden rule: revenue is always recognised from the bank ledger (a credit to Sales 4000). Allocations only add units-out and COGS, they never touch revenue, so nothing gets double-counted.