Azri

A white-label feedback service that people reach by SMS. After a branch visit, a call or a car service, the business texts the customer a personal link; it opens a mobile-first page that checks the token, shows the campaign and its questions rated one to five on star controls, takes an optional comment, submits once and says thank you. Every business has its own monogram, colours, sender ID and copyright line, and a link that is unknown, used or expired gets its own end screen. We built the survey front end and the thin proxy, theming and delivery around it for a CRM that owned the campaigns, the tokens and the texts.

Year: 2026 · Category: customer feedback

Our role

Front end and delivery. The respondent microsite and its state machine, the proxy layer to the CRM's API, per-client white-labelling, and the container build that shipped it.

The problem

Businesses that serve people face to face, such as banks, telecoms, clinics and car services, want to know how a visit went. A form on a website is never opened, a callback survey costs a person's time, and a long questionnaire is abandoned at the first screen. What does get opened is a text message, and what gets answered is a handful of star ratings on a phone.

The constraints follow from that. The link has to be personal and single-use, or the same answer can be sent twice and a forwarded link answers for a stranger. It has to carry enough context for the page to say whose visit this is. And it has to work for several businesses at once, each of which expects the page to look like its own.

The approach

We kept the survey front end deliberately small and let the CRM own everything that was not the page. Campaigns, questions, token issuance, expiry rules, SMS dispatch and result storage all live upstream; the microsite only validates a token, fetches the questions, posts the answers and renders one of four outcomes. State is plain component state, because there is no state worth a library.

Three route handlers forward JSON to the CRM and relay the status and body unchanged, which removed the browser's cross-origin problem without inventing a second API. Each client is a configuration (name, endpoints, logo, primary colour) rather than a fork. The demo takes the same idea one step further: the tenant's look travels in the validation response, so one bundle can show five businesses.

Highlights

  • The respondent's identity is the link: a six-character token from an alphabet with no look-alike characters, checked on every call, spent on the first submit and answered with a distinct end screen when it is unknown, used or expired.
  • White-label to the pixel: the tenant's monogram, colours, sender ID and copyright line arrive with the validation response, so one component tree wears five different brands.
  • The survey keeps the original's quirks on purpose: failures as HTTP 200 envelopes, ratings you can raise but never clear, unrated questions sent as `null`, one submit per link.
  • Around the survey sits a simulated CRM view: an inbox of six personas, a console with KPIs, trend, distribution and per-question averages, a fuzzy-searchable responses feed, campaign pages and the SMS dispatch log, labelled as simulated in its top bar.
  • An API playground built from the original's small developer test page: the three survey calls with editable JSON, the response, status and latency, and a demo clock that fast-forwards expiry.
  • Agent accounts are scoped by the server, not the UI: whatever branch or name a branch member's request carries, the send-SMS call is forced to their own branch and name.
  • Bilingual to the last string: English and Georgian are both complete, in 725 interface strings, and the seed copy, the survey questions and the SMS bodies exist in both languages.

Architecture

  • SMS: The business's CRM sends a text containing a personal one-time link of the form `https://<host>/?id=<token>`. Dispatch, token issuance, expiry rules and result storage all live there.
  • Microsite: Next.js 15 (App Router) on React 19, Tailwind CSS 3.4 and DaisyUI 4 with one custom theme, and Noto Sans Georgian through `next/font`. Every page is a client component and state is plain `useState`: no state library and no i18n library. About 700 lines across 24 files.
  • Proxy layer: Three route handlers forward JSON to the CRM's REST API (validate, submit, get questions), relay the status and body unchanged, disable caching and log both sides. They were added to get around browser CORS, since the first build called the CRM directly from the browser.
  • Upstream: The CRM's REST API, one subdomain per client. It owns campaigns, questions and survey state, and it sends the texts.
  • Deployment: First a static export in a multi-stage Docker build (Node 20, then nginx) with mounted TLS certificates, an SPA fallback, gzip, security headers and one-year immutable caching on static assets. Route handlers cannot run in a static export, so the second version runs Next in server mode behind an nginx reverse proxy under docker-compose.
  • Multi-tenancy: One deployment per client: a config module with the company name, API base URL and endpoint paths, a swapped logo and icon, and the theme's primary colour.
  • The demo: The same flow as a Vite + React single-page app over an in-browser mock of the survey API: seed JSON compiled into the bundle, an in-memory store, a router that answers with the same envelopes, and a theme read from the token's tenant instead of a per-client deployment.

In numbers

Roles: 3 · Screens: 15 · Mocked API routes: 24 · Languages: 2 · Interface strings per language: 725 · Seeded survey links: 286 · TypeScript lines: 12k

Results

  • A customer goes from a text message to a submitted answer in one tap and a handful of stars, on a phone, with no account and nothing to install.
  • A link answers once. A forwarded, reused or old link ends on a screen that says why, in the business's own colours.
  • Adding a business is configuration, not a rebuild: its name, endpoints, logo and colour.
  • The front end stays about 700 lines because everything stateful lives upstream where the business already manages it.

What is used

  • Microsite: Next.js 15 (App Router), React 19, Tailwind CSS 3.4, DaisyUI 4 (one custom theme), Noto Sans Georgian via next/font, Plain component state, no state or i18n library
  • Server side: Next.js route handlers as a thin proxy, JSON over the CRM's REST API, One subdomain of the CRM per client
  • Runtime and delivery: Docker multi-stage build (Node 20, then nginx), nginx with an SPA fallback, gzip and security headers, docker-compose with a reverse proxy, Mounted TLS certificates
  • Demo build: Vite 6, React 18, TypeScript (strict), React Router 6, TanStack Query 5, Tailwind CSS 4, Radix UI, Framer Motion, Recharts 2, react-hook-form + zod, Fuse.js, i18next (EN / KA), date-fns, @portfolio/demo-kit (in-browser mock API)

The demo

A fictional re-skin of a real product Boulder built, running with its backend cut off. Azri, its five businesses and every address and phone number are invented. The survey is the real product's flow; the inbox, the console and the playground are our simulated CRM view around it, labelled as such, because the real campaigns, tokens and results lived in a system that is not ours to show. Everything is answered by an in-browser mock with seed data compiled into the bundle, so data lives in memory and a reload restores the pristine seed. Nothing is sent over a network, and no text message is ever delivered.