Case study · NexGen Wellness
A storefront and a sales pipeline, built as two systems that share one database
A wellness retailer selling massage chairs and foot massagers from a showroom in Jaipur, plus event and venue rentals.
Shark Labs built two systems for NexGen Wellness, a wellness retailer in Jaipur: a statically rendered Next.js 16 catalogue site with a cookieless first-party analytics pipeline and instant lead notifications, and a separate internal CRM that ingests leads from IndiaMART, JustDial, Meta Ads, TradeIndia, Google Business and showroom walk-ins into a single pipeline. The two deploy independently, share one Supabase project through separate schemas, and neither imports code from the other.
- 6 sources
- Lead channels unified into one pipeline
- 0 policies
- RLS policies — the database denies by default
- No cookies
- Analytics without a consent banner
The problem
NexGen Wellness sells physical products from a showroom, and its enquiries arrived from six different places: IndiaMART, JustDial, TradeIndia, Meta Ads, Google Business, and people walking through the door. Each channel had its own inbox, its own format and its own idea of what a lead record was. Nobody could answer how many enquiries came in last week, which channel produced customers rather than noise, or whether anyone had followed up.
There was also no real website — which meant the one channel they fully controlled was the one that didn't exist.
Two systems, deliberately separate
The obvious build is one application with a public front and an admin area behind a login. We split it into two applications that deploy independently and share a single Supabase project through separate schemas, and that decision shaped everything else.
- The public site is entirely static. Nothing an anonymous visitor loads is rendered on request, so a marketing page cannot be slowed down or taken offline by anything happening in the sales tool.
- The CRM has a completely different risk profile. It is authenticated, it holds customer contact details, and it changes weekly. Deploying it should never require redeploying the storefront.
- Neither imports from the other. The CRM reads the site's
quote_requeststable read-only through an ingestion adapter — a database contract rather than a code dependency.
A catalogue that is one file
The product range changes a few times a year. That is the wrong shape for a CMS — a licence, a hosting bill, an upgrade path and an editor to train, for a dozen edits annually.
Instead a single typed array is the source of truth. Adding a product means dropping images in a folder and adding one entry, after which it appears on the homepage, gets its own statically generated page, and enters the sitemap. No step can be forgotten because there are no steps.
// src/data/products.ts — the single source of truth
export const products: Product[] = [ /* … */ ];
// src/app/products/[slug]/page.tsx
export function generateStaticParams() {
return products.map((product) => ({ slug: product.slug }));
}
export const dynamicParams = false;Analytics without a cookie banner
The client wanted to know which channels produced enquiries. The reflex answer is a third-party analytics script and a consent banner. We built a first-party pipeline instead, and the constraints turned out to make it better rather than worse.
- No cookies. Visitor identity lives in
localStorageand session identity insessionStorage, so nothing rides on every request and nothing is readable cross-site. Under most readings that removes the consent banner requirement entirely. - IP addresses are never stored. Only a salted SHA-256 hash, truncated — and with the salt unset the column stays null on purpose, because an unsalted hash of an IPv4 address can be brute-forced in seconds.
- Global Privacy Control and Do Not Track are honoured. For those visitors the tracker silently collects nothing. An enquiry from one still saves; it just arrives without attribution.
- The write endpoint is public, so it validates like one. Every string is length-capped, UUIDs must be v4, UTM keys are allowlisted, and free-form properties are flattened to bounded scalars. It always answers
204, so probing it tells an attacker nothing.
There is no rate limiting in the application, and that was a deliberate call: if the endpoint gets abused, a WAF rule in front of it is the right tool, not a counter inside a serverless function that resets every cold start.
The database denies everything by default
Row level security with zero policies
Postgres row level security is switched on for every table, and no policy is ever written. The effect is that the anonymous and authenticated roles can do nothing at all — no reads, no writes. Only the service role key, which never leaves the server, can touch the data.
This is why the public site has no NEXT_PUBLIC_SUPABASE_* variables at all. The browser talks to a tracking route and a Server Action; it has never held a database credential of any kind. A leaked key from the storefront is not a category of incident that exists here, because there is no key in the browser to leak.
Failing softly, on purpose
Every external dependency degrades to a no-op when its configuration is missing. Without Supabase credentials the tracking endpoint still answers 204 and the quote form falls back to a mailto: handoff. Without a Resend key nothing sends and the enquiry still saves.
Two things come out of that. A developer can clone the repository and run the whole site with no secrets at all. And a marketing site can never return a 500 because an analytics integration is misconfigured — which is the correct trade, because a visitor who can't load the page is a worse outcome than a visit nobody counted.
The enquiry email that can't lose an enquiry
When someone submits the quote form, the sales inbox gets an email. The ordering matters more than the feature: the database row is written first and is the record of truth, and the email is sent afterwards inside Next.js's after() so it runs once the visitor's response is already on its way.
The visitor never waits on a mail provider, and a bounce can never turn a successfully saved enquiry into an error on their screen. A failed insert is reported to the visitor; a failed email is logged and nothing else. The whole email module also never throws — it returns a boolean — because by the time it runs, the person has already been told their enquiry was received.
Six inboxes into one pipeline
The CRM is where the actual business problem gets solved. Leads from IndiaMART, JustDial, TradeIndia, Meta Ads, Google Business and showroom walk-ins land in one pipeline with one shape, and from there become contacts, activities and tasks that somebody owns.
- A per-source ingestion adapter, so a new channel is a new adapter rather than a change to the pipeline.
- A drag-and-drop board over the pipeline stages, with the stage transitions enforced in the database rather than in the interface.
- Business rules implemented as Postgres triggers on purpose — a rule that lives in a trigger cannot be bypassed by a new code path that forgot about it.
- Reporting over the unified pipeline, which is the first time the question 'which channel is actually worth the spend' has had an answer.
What we're not claiming
There are no conversion percentages or revenue figures on this page. We were not given them, and a case study with invented numbers is worth less than one without any — it just takes longer to find out.
What is true and checkable: both systems are live, the storefront is statically rendered, six lead sources arrive in one pipeline, and no database credential has ever been shipped to a browser. When there are outcome figures we're allowed to publish, they'll be added here with the client's name against them.
Built with
Next.js 16
React 19
TypeScript
Tailwind CSS v4
Supabase
PostgreSQL
shadcn/ui
Resend
Zod
Bun
Keep reading
The services this drew on
Service
Web development
Fast, accessible, search-visible sites and web apps on Next.js — built so your team can edit them without calling us.
Service
App development
Web and mobile products from first screen to store listing, and the backend underneath — scoped so version one ships in months, not quarters.
Service
Retainers
Monthly engineering after launch: monitoring, patching, incident response and new features, at a price published on the page.
Article
What a software maintenance retainer actually costs
Most agencies won't put a number on a retainer. Here are ours, the industry ranges we checked them against, and the arithmetic for deciding when hiring someone is cheaper.
Next step
Got something like this?
Tell us what you're running and what isn't working. The first call is free and usually narrows the problem more than it widens it.