One platform that runs a Queenstown members club: memberships, bookings, events, billing, luxury travel itineraries and a member app. It replaced Optix, HubSpot and three other tools, and saves the club $90,000 a year in subscriptions.
Web apps, Mobile apps, Integrations, Automation, Data & reporting
The Mountain Club is a members club in Queenstown, with two venues, about 400 members and a small team. Members come for the desks and studios, but they stay for the people: the events, the introductions, the café and bar, and a concierge that plans luxury trips around New Zealand.
The club ran on a stack of rented software that didn't talk to each other. Optix held memberships, bookings and invoicing. HubSpot held the CRM and every event's attendee list. Itineraries lived in TourWriter and Canva, the community lived in Slack, and event sign-ups came through Webflow forms. Every month, staff copied data between them by hand.
I replaced all five with one system: a shared Postgres database, a background job server, Xero integration for every dollar, and three apps on top. One for staff, one for the travel concierge, and one for members.
Impact
A year in software subscriptions, retired
$90k
Optix, HubSpot, TourWriter, Slack and Webflow, replaced by one system
5 → 1
Background jobs keeping billing, Xero, the café till and email in step
42
Automated tests across the apps and shared packages
20,000+
The problem
Each tool was fine on its own. Together they were, in the club's own words, death by a thousand cuts.
Optix was built for co-working, not a members club. It needed a new plan for every price variation, capped room bookings at four hours, and couldn't split a bill between a company and the person using the desk. The café's till didn't talk to it, so every month someone copied each member's café tab across by hand. HubSpot knew who registered for an event, but not who turned up. And the most valuable question in the business, whether members who make connections stay longer, couldn't be answered at all, because the data lived in five places.
The travel side was worse. A luxury itinerary took two weeks to build in Canva, its pricing was typed into a spreadsheet, the invoices were typed into Xero, and reconciling supplier bills afterwards took about a month.
How it grew
July 2025Glue between the tools
QR check-ins, Webflow event registrations synced into HubSpot, Optix booking integrations, then an AI marketing pipeline. A year inside the club's operations showed me the tools themselves were the problem.
June 2026The itinerary builder
A replacement for TourWriter and the Canva deck: day-by-day trips, three supplier pricing modes, a live preview and a shareable, branded itinerary.
July 2026Xero, and a monorepo
Trip invoicing, payment schedules and supplier bill reconciliation in Xero. Then the app was restructured into a Turborepo monorepo, ready for everything else.
August 2026Replacing Optix
One identity for every person, plans and entitlements, venues and bookings, the charge ledger, the staff console and the first member app. The background job server went live at the end of the month.
September 2026Community
Channels to replace Slack, push notifications, member onboarding, an engagement score for every member, and the weekly What's On email.
October 2026Money, end to end
Café sales from the Lightspeed till, direct debit, monthly invoicing to Xero, private hire, and the member app with staff on TestFlight.
How it's built
The club will never have more than a few thousand members, so the enemy was complexity, not scale. Everything defaults to boring: one Postgres database is the store, the queue and the audit log. Business rules live in shared packages with no framework code in them, so the staff console, the member app and the job server all run the same logic.
The till, Xero and members' own actions feed one Postgres database. Shared packages hold the rules, and three apps and a job server sit on top.
Monorepo: Turborepo and pnpm, with six apps and 25 shared packages. Domain logic lives in @mc/services, with no framework code in it, so the Expo app and the job server can use it too.
Web: Next.js 16 and React 19 for the staff console, the itinerary portal and the public site, with a shared design system in @mc/ui and @mc/brand.
Mobile: Expo and React Native for the member app, built and shipped with EAS.
Data: Supabase Postgres with Drizzle, 370 migrations and 276 row-level security policies.
Background work: a pg-boss job server on Fly.io, traced end to end with OpenTelemetry into Grafana.
Integrations: Xero, Lightspeed, GoCardless through Xero, Expo push, Resend and Slack.
One record for everyone
The core decision was one identity. Staff, members, leads, event guests and itinerary travellers are all the same Person, with roles layered on. A lead becoming a member is a role being added, not a record being copied, so the history stays in one place.
The second was one ledger. Everything a member uses becomes a charge: a room booking, a coffee on account, a check-in, a paid event, the plan fee itself. It runs through one pipeline that prices it, applies their plan's allowances and discounts, works out who pays, and writes the charge. It always writes the charge, even at $0, so the club can finally see how every member actually uses the place.
Who pays is the hard part. A company might cover three people on three different plans, while one of them pays for their own coffee. Plans attach to the person's seat, bills go to an account, and the payer is worked out per charge and then frozen.
A member's record in the staff console. The summary is written from their real activity, and everything about them is one tab away: plans, bookings, events, visits, usage, invoices and connections.
A job queue built on Postgres
Most of what the platform does happens when nobody is looking: billing runs, Xero pushes, café sales, reminders and push notifications. So the jobs had to be dependable, and easy to reason about when something went wrong.
I started on Inngest, then moved everything to pg-boss on the same Postgres database. Jobs share the app's connection pool, its transactions and its traces, so there's one system to understand. The job server is a single Node process on Fly.io with 42 scheduled queues.
The real work items are rows, not messages. When something happens, the change and the work it creates commit in the same transaction: a booking and its confirmation email, an invoice and its Xero push. A member is never told about something that rolled back.
Claiming: each tick claims a batch with FOR UPDATE SKIP LOCKED and counts the attempt before any network call, so a row that crashes the worker can't loop forever.
Retries: one shared schedule of 1, 5, 15, 60 and 240 minutes, then the row is marked as failed for good, with the provider's error, where staff can see it. Retry-After is always honoured, and capped, so a hostile header can't park the worker until tomorrow.
Nothing happens twice: emails carry idempotency keys, webhooks are unique on the provider's event id, and every Xero push sends a key derived from the invoice and its content.
Circuit breakers: five failures in a row and the breaker opens. Work that was claimed but never tried goes back without spending an attempt, so an outage doesn't use up everyone's retries.
Watched: a stall watchdog restarts the server if nothing finishes for ten minutes, and a digest lands in Slack every morning with one line per queue.
Every job follows the same lifecycle, from the transaction that creates it to a terminal state staff can see. The morning digest says what ran, what failed, and why anything was missed.
Billing, through Xero
Xero stays as the system of record for money. The platform's job is to get the right invoice into it, every time, and to know straight away when it's paid.
Members
At 2am, the billing run renews every plan that's due, one transaction per membership: it writes the plan fee as a charge, refills the allowances and moves the period on. At 3am, invoicing picks up every account's pending charges, from plan fees to café tabs, and turns them into one invoice per account.
The local invoice commits before anything is sent to Xero. The worst case is an invoice that isn't in Xero yet, which staff can see and retry, never an invoice in Xero that the platform doesn't know about. Each line is coded to the right revenue account and tracking category, and invoices over Xero's 50-line limit are split.
Xero only allows 5,000 calls a day, and the monthly run uses about 1,200 of them. A pacer reads Xero's own rate-limit headers and keeps a reserve of 250 calls, so a member can still pay an invoice at 11pm. When Xero says a payment has landed, a webhook settles the charges, confirms any held booking and releases event seats.
Invoices in the staff console. Each one shows where it is in Xero and whether it reached the member, so a stuck invoice is a filter away instead of a phone call from accounts.
Itineraries
Travel billing works differently. Each confirmed trip gets its own revenue and expense accounts in Xero, so every trip's margin can be read straight from the books. Suppliers are priced one of three ways: cost plus markup, retail with commission, or retail with none. The client only ever sees the price, never the cost or the margin.
The client is invoiced against a payment schedule, usually a deposit on booking and the balance before the trip. Supplier bills come back in from Xero and are matched to the trip's cost lines: exact matches first, then combinations of lines, then an AI suggestion that a person has to accept. The month of reconciliation after each trip becomes a review of what's left.
A trip's money: what's been collected from the client, what's been paid to suppliers, and the margin on every line.
Three portals
The staff console
Members, events, private hire, bookings, check-in, plans, products, marketing and billing, in one place, with permissions by role. The dashboard tells each person what needs them today: a host sees tonight's guest list, accounts sees invoices stuck short of Xero.
The itinerary portal
The concierge builds a trip region by region and day by day, from a library of suppliers, stays and experiences. Pricing is calculated as she goes, the margin is always on screen, and the same trip renders as a teaser, a full day-by-day itinerary or a simple agenda, in either the Mountain Club or Summit brand. What used to take two weeks in Canva now comes out of the builder.
Building a South Island trip. Each day is split into schedule, stays and transport, and the client total and margin update with every line.
The member app
Members book rooms, register for events, check in, see their plan and their invoices, and message each other. The home screen is built around connection, because that's what the club is really selling: a new member to meet, the club's pick of the week's events, and a member perk.
The member app. Home leads with someone new to meet, and Events shows what you're going to and what's picked for you.
What I'd take from it
The biggest win wasn't any one screen. It was putting the whole club in one database with one identity and one ledger. Once every coffee, booking, event and invoice is a row that points at a person, the questions the club could never answer, like who's slipping away and whether connections keep people, become a query.