Pixeldev

Work/OnCloudWine

Wine club software that started as a WordPress plugin and outgrew it.

Wineries run their clubs on a cycle: build a release, let members tweak their selection, charge every card on file, then pack and ship hundreds of orders. Most do it with spreadsheets, a POS and a lot of manual card processing. OnCloudWine does the whole loop, and I have built every version of it.

Product
OnCloudWine
Industry
Wine and hospitality
Services
Product design, SaaS development, integrations
Stack
Next.js, TypeScript, PostgreSQL, Prisma, BullMQ, Redis
Timeline
October 2024 to now
OnCloudWine home dashboard with member KPIs, a club composition chart and a map of members by Australian state
The home dashboard: members, signups, holds and cancellations over the chosen period, with the split across clubs and where members live.
980commits in the production platform
22background job queues behind syncs, payments and shipping
77database models across CRM, releases, payments and fulfilment

The problem

A wine club looks simple from the outside: members pay, wine arrives. Behind it, a winery is juggling several club tiers, members who pause or cancel, members who want two more bottles of the Pinot and none of the rosé, cards that have expired since the last release, and a shipping run that has to match exactly what was charged. Before OnCloudWine, a typical winery exported its member list to a spreadsheet, keyed each card into the POS one at a time, and spent the next week fixing declines and mismatched shipments by hand.

The tools wineries already use don't think in releases. A POS knows about customers and orders. A WooCommerce store knows about products. Neither knows that the Autumn release goes to three clubs, that it charges 1,100 cards on one afternoon, and that a member in Tasmania picked up instead of shipping.

Version one: a WordPress plugin

The first OnCloudWine, built from October 2024, lived inside wp-admin. A React app on top of WooCommerce, where each club was a WordPress user role, products came from the store catalogue, and a release walked through five steps: create, select products, create orders, process cards, archive. Square handled the cards.

It worked for one winery on one site. It also showed me the limits quickly: the data model was WordPress's, every winery needed its own install, and the slow parts (charging hundreds of cards, generating packing lists) were running inside PHP requests and WP cron.

The original OnCloudWine WordPress plugin inside wp-admin, showing a release card with step progress and product stock checks
Version one, inside wp-admin. Clubs were WordPress roles and each release card tracked its own five steps and stock.

What I built next

In mid 2025 I rebuilt it as a standalone multi-tenant platform: a Next.js and TypeScript monorepo with a staff dashboard, a member portal, a public API, a marketing and docs site, and a BullMQ worker on Redis. Every table is scoped to an organisation, and each winery can run the dashboard and portal on its own domain.

The WordPress plugin didn't go away. I rewrote it as a thin client of the public API, so a winery's WooCommerce site can show members their upcoming release and let them change their selection, while the data lives in one place.

  1. 1Contacts CRM with tags, tasks, task automations (birthdays, anniversaries), discounts and saved cards.
  2. 2Clubs and memberships with active, on hold and cancelled states, signup locations and goals.
  3. 3A release wizard: products and quantities, which items are required or adjustable, target clubs, shipping or pickup.
  4. 4Member customisation from the portal, the WooCommerce plugin or by staff, before anything is charged.
  5. 5Batch payments through Square or the winery's own Stripe account, with failures turned into follow-up tasks and card update emails.
  6. 6Fulfilment: packing lists, FedEx and Shippo labels, VinoShipper for compliant US shipping, tracking, and product swaps with automatic charge or refund of the difference.
  7. 7Syncs with Square, WooCommerce and Mailchimp, plus Stripe billing for the wineries themselves.

The hard part: charging a thousand cards exactly once

Processing a release is the moment the product earns its keep, and the one place a bug costs real money. Staff walk through a wizard, confirm with a one-time code sent to their email, and a background job charges every eligible member. The database allows exactly one payment row per contact per release, so a retry reuses the row and a completed payment is never charged twice. Every Square charge carries an idempotency key built from the organisation, release, contact, amount and currency, and every Stripe charge carries its own key per contact, so a worker that dies halfway through can be rerun safely.

The platform fee is the other subtle bit. On Stripe it rides on the charge as an application fee. On Square it can't, so each fee is recorded in a ledger and added to the winery's monthly Stripe invoice. Around that sit encrypted OAuth tokens for every provider, a Square token refresh that runs both on demand and on a 12 hour schedule, and progress reporting so staff can watch a run of 1,100 payments land.

Process Payments wizard showing the confirmation step with release details, contact readiness and outstanding issues
The last check before charging: who is ready, who has an expired card or missing address, and what gets skipped.

Where it is now

The platform has been in production since September 2025 and now 34 wineries run their clubs on it. As one winery's club manager put it, release day used to take the whole team two days, and now it is an afternoon with time left over to answer member emails.

In August 2026 I started moving it onto a newer foundation: Next.js 16, tRPC, Better Auth and Prisma 7, ported one domain at a time with a written migration log. The rebuild adds what the first platform was missing, a proper test suite: 130 unit tests, 238 database tests and 32 Playwright specs so far, plus a rerun of the unit suite in Sydney and Los Angeles time zones. That run alone caught four bugs where a release date shifted by a day.