Skip to content
← All work
Multi-tenantAI AgentsSaaSB2B Travel

ANZ Global Travel & Events, B2B wholesale platform for travel agencies across Australia, New Zealand, and Fiji

A multi-tenant wholesale platform that lets 190+ agencies and 5,000+ agents build, price, and confirm itineraries in one place.

ANZ Global Travel & Events · B2B Travel Wholesale · 2025·Production·Multi-tenant
ANZ Global Travel & Events, hero

Project Overview

ANZ Global Travel & Events is a B2B travel wholesale platform. ANZ buys travel inventory (hotels, activities, car hire) at wholesale rates and resells it through a network of travel agencies across Australia, New Zealand, and Fiji. The platform we built is the web app that connects ANZ to those agencies. Tourists never see it. Only agency staff and ANZ admins use it.

In one line: the Shopify of travel wholesaling. ANZ is the platform, agencies are the merchants, agents are the staff building quotes for end customers.

Client Background

ANZ Global Travel & Events sits in the wholesale middle of the travel supply chain. They negotiate net rates with suppliers, package the inventory, and distribute it through more than 190 agencies that employ more than 5,000 agents across the region. The business needed a single platform where agencies could browse the catalog, build quotes for their walk-in clients, generate proposal PDFs, confirm bookings, and track commission. The previous workflow ran on spreadsheets and email.

ANZ's own staff also needed a console. They had to manage products, approve new agencies, invite users, run bulk imports, mark commission payouts, and answer support tickets. None of that was in one place either.

The Problem

Agents at small and medium travel agencies were spending hours per quote. A quote required pulling rates from multiple suppliers, mapping a city-by-city itinerary, applying markup, calculating GST, generating a branded PDF, and tracking which clients had accepted. Most of this happened in Excel. Mistakes were common. Pricing got out of date. Different agents at the same agency could not see each other's work. Commission tracking was manual and lossy.

For ANZ as the wholesaler, the lack of a shared platform meant they could not see what agencies were quoting, what was converting, and where leakage was happening. Onboarding a new agency meant emailing PDFs of product catalogs and hoping the rates were used correctly.

Challenges

  • Multi-tenant isolation: 5,000+ agents from 190+ agencies needed to see only their own data, never another agency's.
  • Three distinct user roles (ANZ admin, agency admin, agent) with different permissions, screens, and route trees.
  • Real-time pricing across hotels, activities, and packages, with markup the agent could tune per quote.
  • AI-assisted itinerary generation that produced real city and activity selections from the live catalog, not invented IDs.
  • A booking lifecycle that moves from draft to quote to confirmed to archived, with a different reference number scheme at each stage.
  • PDF proposal generation with client-facing pricing only (gross), with wholesale costs strictly hidden.
  • Bulk import and export so agencies could bring existing client and product data, and so ANZ could feed the system from supplier spreadsheets.
  • A user-creation pipeline that handled four different signup paths (admin, team invite, admin-direct invite, public registration) without orphaning anyone.

Our Approach

We separated the platform into three role-shaped surfaces: a public marketing site, the agency-facing app at /agent/*, and the admin-facing app at /admin/*. Each surface owns its own dashboard, navigation, and detail pages. Permissions live in the database and are enforced at the row level, not in the UI.

We modeled the quote as the single core object. A quote is an itinerary, and an itinerary becomes a booking. The same table holds both, with a status field that drives the rest of the application.

Pricing is computed from net rates and markup, with two strict contexts: agents and admins see net plus markup, clients see gross. The two contexts never mix in one screen.

The AI quote builder reaches into the real catalog rather than guessing. The model takes a natural-language brief, returns three options, and each option references real activity IDs the agent can immediately accept.

What We Built

User Experience

A 3-step quote wizard at /agent/quotes/new. Step 1 collects trip details and the client. Step 2 lets the agent pick a hotel and activities per city, with a sticky sidebar showing the running total. Step 3 reviews the breakdown and lets the agent save as draft, save as quote, or confirm a booking. The same wizard runs for admins, with a banner indicating they are acting on behalf of a specific agent.

The AI builder is a dialog. The agent types a one-line brief or picks a quick prompt. Three full itinerary options come back, each with cities, nights, and a price per person. Selecting an option fills the form and drops the agent into Step 2.

Admin Operations

A separate admin console at /admin/* covers product management (activities, hotels, packages), agency approvals, user invitations, commission marking, support tickets, and reporting. Admins can build quotes on behalf of any agent at any agency. The pricing and itinerary code is shared with the agent wizard, so admin-built quotes follow the same rules as agent-built ones.

Automation

A PostgreSQL trigger on every new auth user routes the signup down one of four paths: ANZ admin, team invite by email match, admin-direct invite by metadata, or public agency registration. The right tables are populated automatically and the right approval state is set.

Background jobs handle bulk import of agents and products, batched user creation, welcome emails, and post-import approval sweeps. Progress streams back to the UI via Supabase Realtime so the admin sees a live counter, not a frozen spinner.

Reporting

The admin dashboard shows active agencies, total agents, total quotes, confirmed bookings, total revenue, pending approvals, pending commissions, and open tickets in one view. Conditional alert cards link straight to whichever queue needs attention.

Agencies see their own version: their quotes, their bookings, their team's volume, their commission balance, their wallet activity.

Integrations

  • Supabase handles authentication (email and password plus Google OAuth), database, storage for logos and product images, and realtime channels.
  • Anthropic Claude generates itinerary options with an OpenAI fallback when the primary model is unavailable.
  • Trigger.dev runs the background import jobs and the post-confirmation work.
  • A payment integration handles deposit and instalment collection against confirmed bookings.

Security

Row-level security on every multi-tenant table. Agency admins see their agency, agents see only their own quotes, ANZ admins see everything. The PDF proposal pipeline strips wholesale pricing before rendering, so client-facing documents only ever show gross. Account deletion is blocked for admin roles. Password reset signs out other sessions.

Performance

Catalog reads are paginated by URL state, so deep links survive refreshes and back-buttons. Bulk operations run in batches of 100 against the auth admin API. Realtime channels are scoped to single rows, so the dashboard does not subscribe to the full database.

Technical Architecture

The platform is a Next.js App Router application backed by Supabase. Three role-shaped route trees drive the UI. The same Postgres schema serves all of them; access is governed by row-level security policies, not by client-side checks.

The itineraries table is the central object. It stores quotes and bookings as the same row, with a status field driving the lifecycle. The cities JSONB column carries the full city-by-city structure, including selected hotels, activities, and car hire.

Key Decisions

  • One table for quotes and bookings. We considered splitting them but chose a single itineraries table with a status enum. The tradeoff is that the row carries fields that only matter for one phase. The win is that converting a quote to a booking is a status change, not a data copy, and history is automatic.
  • Row-level security as the enforcement layer. Permissions could have lived in the application code, but Postgres RLS gives us one source of truth that holds even if a client calls a server action with the wrong context. The cost is more careful schema work upfront. The benefit is that a bug in a client component cannot leak another agency's data.
  • AI uses real catalog IDs only. The itinerary builder is constrained to reference IDs that exist in the database, rather than free-text suggestions. This rules out the "model invented a tour" failure mode at the cost of a smaller pool of activities per generation.
  • Net and gross are strict UI contexts. Agents and admins see net plus markup. Clients see only gross. We considered a toggle and rejected it because the risk of leaking wholesale pricing into a client document was too high.
  • Trigger-driven user creation. A single PostgreSQL trigger routes new auth users down the right path based on metadata. This puts the routing logic in one place near the data, instead of scattered across multiple server actions.

Results

  • Agencies can build a full multi-city quote in minutes rather than hours.
  • Quote-to-booking conversion is a single status change rather than a data copy.
  • Admins have one view of every quote, every agency, every commission, and every payment.
  • Multi-tenant isolation is enforced at the database, not in the UI, so a UI bug cannot expose another agency's data.
  • The same wizard works for admins building on behalf of an agent, so internal support cases use the same path as production.
  • Bulk import lets a new agency arrive with hundreds of agents and be live the same day, instead of email-by-email onboarding.

Project Highlights

  • A 3-step quote wizard with an animated step transition and a sticky live total.
  • An AI itinerary builder that returns three real, bookable options from a one-line brief.
  • A multi-tenant data model serving 190+ agencies and 5,000+ agents with row-level security.
  • A PDF proposal pipeline that exposes only client-facing gross pricing.
  • Bulk agent import with batched user creation, welcome emails, and realtime progress.
  • A four-path new-user trigger that handles admins, team invites, admin-direct invites, and public agency registrations.

Screens and User Flows

The flow an agent runs every day:

The quote lifecycle on the platform:

Future Growth Opportunities

  • Payment collection inside the platform with auto-generated deposit and instalment schedules, removing the manual side-channel that handles money today.
  • A supplier-facing surface so inventory updates can flow without admin re-entry.
  • Wallet redemption against future bookings for top-tier agencies.
  • A reporting layer that turns confirmed booking data into pipeline forecasts and supplier scorecards.
  • Mobile views tuned for agents working trade shows or supplier events away from a desk.
  • An audit log surface exposed to agency admins so they can see who on their team did what.
The stack
Frontend
Next.jsReactTypeScriptTailwindshadcn/uimotion
Backend
SupabasePostgreSQLRow-Level Security
AI
Anthropic ClaudeOpenAI
Infra
Trigger.devSupabase RealtimeSupabase Storage
Auth
Supabase AuthGoogle OAuth

Want one of these for your team?

45-min call, fixed quote in 72 hours, code in production by week 4.

Book a 45-min call →

Core MVP shipped to production by week 4.

or send a 2-min Loom →or email hello@obsidiancode.io