A tackle shop's knowledge, built into the storefront
This is an e-commerce platform, built and in pre-launch, for an Australian fishing & outdoor-gear retailer — a real shop with a real counter, whose staff have spent years answering the same question: what do I actually need to catch this fish, here, this month? The site is built to answer that at scale. The backend is live — the catalogue and stock sync automatically from the shop's till, the owners run the admin daily, and a waitlist is filling — while the storefront itself sits behind a coming-soon page until launch. It is not a generic store template with products dropped in; it is a 7,000-product catalogue wired to a fishing-intelligence layer — species tagged to gear by relevance, a seasonal model of what's active at each location, a curated setup builder, and community catch reports — sitting on top of a checkout and an inventory sync built to keep the online store honest against the till in the shop. The retailer owns all of it. Everything below is described against the running code and the production database, as mechanisms — not a pitch.
In plain terms
An online tackle shop that gives customers the same straight answer your best staffer would give at the counter.
The counter knowledge, built in
It tells a customer what gear actually catches which fish, where, and what's biting this month — and adds the full kit to the cart, so they buy instead of guessing.
The till and the website agree
The catalogue and stock sync automatically from the in-store till, and every price is re-checked server-side before a charge — so once the store opens you don't oversell, refund, or quietly lose money.
Yours, and it can't make things up
You own the whole platform and run it yourself; the AI only works from your own shop data, never invents catches, and falls back to a safe version if it's ever unavailable.
The problem
A tackle catalogue is a genuinely hard commerce problem hiding inside a simple one. Seven thousand products — rods, reels, lines, hundreds of near-identical lures separated by a two-gram weight and a colour — are useless as a flat list. The value the shop adds at the counter is judgment: this rod, that reel, this line class, for that fish, off those rocks, in this month. A default store platform can't hold any of that. It can hold a name, a price, and a photo, and it leaves the customer to guess — the exact guesswork that sends people back to a Facebook group or a phone call instead of a cart.
There is a second, quieter problem underneath it: the online store and the physical shop share one pool of stock. Sell a reel online that someone just bought at the counter and you have an oversell, a refund, and a lost customer. The point-of-sale system is the source of truth for stock, price, and fulfilment — but it speaks an older SOAP dialect and syncs on its own schedule. A storefront that ignores that reality looks fine in a demo and breaks in the first busy week. So the brief was two things at once: put the shop's knowledge into the catalogue, and keep the catalogue truthful against the shop's own systems — without renting either capability from a platform that would own the data and the roadmap.
What we built
The platform is a Next.js storefront over a Postgres database whose schema is the actual point of the project: 55 tables across 30 domains that model fishing knowledge, not just a product feed. Species are tagged to gear by relevance tier. A geographic hierarchy and a monthly seasonal model drive a 'what's biting now' surface and AI-written location reports. A setup builder assembles complete rod-reel-line-lure rigs. Faceted search reads structured specs and species tiers. And underneath, a bi-directional sync keeps the catalogue, stock, pricing and order fulfilment aligned with the in-store point-of-sale. Every AI touch is grounded in the shop's own data and degrades to a safe default if the model is unavailable — the store never breaks because a model call did.
Relevance-tiered species intelligence
The heart of the catalogue is a join table that tags each product to the species it's for, at one of three relevance tiers — primary, secondary, or compatible. A snapper rod is primary for snapper, compatible for flathead; a soft plastic is primary for the fish it imitates. There are over 2,700 of these product-to-species tags across the live catalogue, plus a parallel relevance model for fishing techniques. This is what lets the site rank a species page by what actually works rather than by what happens to mention the fish in its name, and it's shop-curated data, not string-matching on titles.
A national geographic hierarchy
Locations aren't a flat dropdown — they hang off a self-referencing region tree (country → state → region → sub-region) with a denormalised breadcrumb path on every node so a hierarchy lookup is O(1). Species carry an array of the Australian states they're found in, and a reference table of twelve water types (inland, estuarine, coastal, offshore) replaces what would otherwise be a brittle hardcoded enum. The result is that filtering scales to a national catalogue: show me lures for a species that's native to my state, fished in estuarine water, without any of that being wired to one region.
The seasonal 'what's biting now' model
Each location-species pairing carries a twelve-month activity strip — a score from 0.0 (absent) to 1.0 (peak) for every month, plus the peak months and the proven techniques for that fish at that spot. The storefront reads today's month against that model to tell an angler what's active right now, how strongly, and what to throw at it. A species page aggregates the same data the other way — taking the maximum score across every location for each month — so a fish's whole season reads at a glance. It is a structured seasonal model the shop maintains, not a guess and not a live feed it doesn't control.
AI-written monthly location reports, grounded in the data
A monthly cron builds a data-driven report for each location from the seasonal model — what's biting, what's rising into next month, the water-temperature and tide context — and then hands that structured brief to Claude (via the Vercel AI Gateway) to write it up in the voice of the shop's head guide. The prompt is strict: ground everything in the supplied species, scores and techniques; never invent catch numbers, dates, named marks or fake angler quotes; return structured sections for intro, where-to-fish, per-species tactics, a tip and an outlook. If the model call fails, the report falls back to the data-templated version — the report is never blocked on the model, and the model is never allowed to make things up.
The setup builder — complete rigs, not loose parts
A curated setups engine assembles whole rod-reel-line-lure combos for a species and technique, each item tagged with its role (rod, reel, main line, leader, lure, terminal tackle, accessory), a required-or-optional flag, and an alternative note ('or any 4000-size spinning reel'). Setups carry a skill level and a budget tier, so a beginner and an experienced angler on different budgets get different, complete answers to 'what do I need'. There are twenty of these built, each one the counter conversation turned into a page that adds itself to the cart.
Faceted search over structured specs, not just keywords
Search runs on Meilisearch against a denormalised document built from Postgres. It filters on the things that actually separate near-identical tackle — species by relevance tier, technique, water type, lure style, hierarchical location, brand, and the structured spec fields that matter per category (rod power and action, reel gear ratio and max drag, line break strength, lure type and size). The default browse order is a deliberate composite quality score — in-stock, then has-image, then featured, then units sold — so every listing leads with product a customer can actually buy and see, instead of out-of-stock imageless records. Explicit sorts override it; text relevance leads on searches.
Two-way sync with the in-store point-of-sale
The catalogue is kept truthful against the shop's Retail Express point-of-sale by a set of scheduled jobs: products and pricing sync down on their own cadences, stock refreshes every fifteen minutes, and paid web orders are set to push up to the POS, with fulfilment status pulling back to flip an order from paid to shipped — closing the loop the order push opens. Products, pricing and stock sync down over Retail Express's REST API and run live today; the order-push and fulfilment-pull direction speaks a GZIP-wrapped SOAP dialect with bulk-call throttling and is fully built — waiting on the shop's SOAP credentials before it goes live. The integration snapshots each paid order into a durable outbox so nothing is lost until those credentials land, and is idempotent on webhook replay.
A stock buffer that prices in the sync gap
Because in-store sales and the next stock sync are up to fifteen minutes apart, the available-to-sell figure the customer sees is deliberately conservative: on-hand minus reserved minus a small safety buffer. It trades two units of visible inventory for the elimination of roughly 95% of oversell risk in that window — a specific, defensible decision written down in the integration architecture, not an accident. Stock itself moves through an immutable inventory ledger (sales, returns, adjustments, purchase orders) that's only ever appended to, never overwritten.
Server-authoritative checkout on Stripe
Checkout never trusts the browser on anything that touches money. The create-intent endpoint recomputes the line prices, the shipping rate and the GST component server-side from the same shipping-quote logic that produced the customer's displayed rate — the client-submitted shipping cost is used only as a tamper cross-check, and shipping rate ids are derived from the service name so a recompute can't bill the wrong rate. Stock is reserved inside the order transaction, so a race to the last unit fails cleanly rather than overselling. Payment runs on Stripe Payment Intents; the webhook parses the raw body before verifying the signature, transitions the order to paid, and enqueues the POS push.
Australia Post rates with a fallback that never blocks checkout
Live shipping rates are built to come from Australia Post, quoted once and recomputed identically at checkout so the price is consistent end to end. When the Australia Post key isn't set or the service is unreachable, the quote falls back to weight-based flat rates and the store reconciles at fulfilment, so a customer can always complete an order — the flat-rate path is what's in effect until the Australia Post key lands in production. Dangerous-goods products (lithium batteries, pressurised cans, marine fluids) are flagged in the schema and restricted to road carriers, because Australia Post rejects them on air freight; the constraint lives in the data, surfaced on the product page and at checkout rather than discovered after the sale.
Community catch reports and an operator-owned admin
Anglers can submit catch reports and photos tagged with the species, location, technique and the actual gear used — moderated behind an approval flag before they surface. And the whole store is operable by the shop, not the developer: an admin engine manages inventory, orders, discounts, the species library and its photos, lure-style tagging, and store settings that change without a redeploy (free-shipping threshold, category display names, business details) because they live in a database row the operator edits, not an environment variable that needs a rebuild. Reversible 'hide' flags let staff pull a species or product off every storefront surface without deleting its data.
Observability and honest imagery
A health endpoint checks the database and every critical credential and is the target of an external synthetic monitor; error paths report through a Lead-Engineer-style Slack alert with a runbook anchor, not a bare stack trace. Product imagery runs through its own barcode-keyed ingestion pipeline that tracks each distributor asset's match method and confidence, and — because a 7,000-line catalogue is never fully photographed at once — a graceful-degradation field can borrow a representative image from a same-model record, kept in a separate column so real-coverage metrics stay honest and clear themselves the moment a genuine image lands.
How it fits together
How the storefront knows what a tackle shop knows
Read top to bottom: the shop's data and live stock sit at the base, a fishing-knowledge layer turns it into judgment, and the storefront surfaces sell it — with the money and stock kept honest underneath.
Storefront surfaces (what the angler sees)
Fishing-intelligence layer
Commerce integrity
Data foundation
Every AI touch is grounded in the shop's own data and falls back to a safe default — the store never breaks because a model call did.
The outcome
The store works because the catalogue knows things a catalogue usually can't. Species tagged to gear by relevance, a seasonal model of what's active where and when, complete setups instead of loose parts, faceted search over the specs that actually separate near-identical tackle — that's the shop's counter knowledge, made browsable. Underneath it, a checkout that recomputes every dollar server-side and a point-of-sale sync that keeps the online stock honest against the till mean the intelligence sits on top of commerce built not to quietly lose money. Every AI touch is grounded in the shop's own data and falls back to a safe default when the model isn't there. We describe only what is built, and mark what is still filling in — the catalogue and stock sync from the till are live and the owners run the admin daily, while the storefront sits behind a coming-soon page and a filling waitlist until launch; imagery is being ingested pack by pack; some seasonal data is a defensible model rather than a live feed. We claim no number we didn't read out of the production database. The retailer owns all of it, and is preparing to open.
Built with
Next project