Back to Case Files
BNR-CF-03Restaurant · Web Suite·Hospitality·Sousse, Tunisia·2026

Kinben Sushi

A Next.js 16 site for a Sousse sushi bar, built on two scroll-scrubbed 120-frame sequences, a 37-dish menu that orders over WhatsApp, and a QR loyalty passport stamped at the counter through its own Express and MongoDB API.

Visit live site
Kinben Sushi — Restaurant · Web Suite homepage built by BreakNRoot

37

Menu dishes

240

Film frames

26

API endpoints

Kinben Sushi is a small sushi bar on Rue des Orangers in Khezama Est, Sousse, open from 13:00 to 23:00 every day except Wednesday and serving gunkan, crispy rolls, futomaki, sashimi and tataki, chirashi, poke bowls and sharing platters of 29, 44 and 70 pieces for delivery, takeaway and dine in. The site is built as a seat at the counter, in the black, white and red the restaurant already presents itself in, with Japanese used as typographic accent: vertical tategaki type, red hanko seals and an enso brush circle that draws itself while the page loads.

The home page reads as a single omakase. It opens on a 120-frame overhead sequence that travels across a slate of sushi, drawn to a canvas from the scroll position, and later plays a second 120-frame sequence in which rice, salmon and nori assemble into a nigiri on a set crushed to true black, so that against the #000 page the pieces hang in space with no frame around them while three captions (the rice, the fish, one piece) scrub in and out. After it, ScrollTrigger pins the viewport on desktop and pans six full-screen course spreads sideways, one per menu category apart from the platters, a CSS kaiten belt carries every plate past, and the page continues through the platters, a loyalty passport, a prize wheel, a twelve-post Instagram wall and a footer with the map, address, phone and hours.

Behind the page sit two more applications. Counter staff use a Next.js back office to scan the QR on a customer's passport, add stamps, redeem rewards and edit the reward tiers, and both the back office and the site's passport talk to an Express and MongoDB API that holds the programme, the members and an event log. The site, the API and the back office run on the agency's own VPS as three Docker containers on Node 22 behind nginx. Native Apple Wallet and Google Wallet passes are implemented in both the site and the API but are not yet provisioned with the restaurant's Apple and Google credentials, so for now the passport lives on the site and at the counter rather than in a phone wallet.

// challenge

The Challenge

A counter-service sushi bar sells freshness and handwork, and a grid of product photos flattens both. The site had to show food being assembled, on phones as well as desktops, without the weight of video or the stutter of seeking an mp4 on every scroll tick in Safari. It had to carry the full printed menu with real prices in Tunisian dinars and let a customer send an order and a table request without an account or a card checkout, around opening hours that skip Wednesday entirely. And it needed a loyalty card that a customer keeps on their phone and staff can stamp at a busy counter, with no app to install and no customer login, and the public site could not depend on the counter API or a wallet provider being connected on day one.

// solution

What We Built

We built the public site on Next.js 16 with React 19, TypeScript and Tailwind CSS v4, using GSAP ScrollTrigger for the two canvas sequences, the pinned course pan and the section reveals, and Lenis on the GSAP ticker for smooth scrolling. Both sequences run through one ScrollFilm component that loads progressively and serves separate desktop and mobile frame sets. Ordering needs no backend: a module-level cart store feeds a drawer that composes a prefilled WhatsApp message, with an optional table booking that only offers the days and half-hour slots the restaurant is open. The counter side is an Express 4 API on Mongoose 8 with Program, RewardTier, Member, EventLog and WalletRegistration models, where a stamp is one atomic increment and a redemption checks and deducts the balance in a single conditional update, plus a separate Next.js 16 back office whose camera scanner uses the browser BarcodeDetector and falls back to jsQR. Wallet issuance is written for both platforms: ten-minute HMAC download tokens, a member serial derived by HMAC from the contact, a passkit-generator .pkpass, an RS256 Google save link, and on the API side Apple's pass web service with APNs background pushes and Google object updates after every stamp.

// outcome

The Outcome

kinbensushi.tn is live on the agency's VPS. What shipped is four page routes (the home page, a searchable /menu ordering grid, the /jeu prize wheel and a /carte web card), all 37 dishes in seven categories orderable into a WhatsApp message with an optional table booking, and a loyalty programme served live from MongoDB: the passport reads the ten-stamp programme and the member's balance from the API, and staff scan, stamp and redeem from the back office, whose dashboard charts stamps and rewards over the last fourteen days. Search and answer engines get Restaurant, Menu and HowTo JSON-LD generated from the same data the pages render, a sitemap and robots policy that keep /carte and /api out of the index, and an llms.txt summary. Native Apple Wallet and Google Wallet passes are built on both the site and the API but not switched on: they wait on the restaurant's Pass Type certificate and Google issuer account.

Engineering log
[01]

Canvas scroll sequences

Two 120-frame image sequences are drawn to a canvas from the scroll position instead of seeking a video element. The first frame loads first, then eight spread keyframes, then the rest in batches of twelve, and the nearest loaded frame is always drawn, so a slow connection gets coarser motion rather than a blank. Desktop and mobile sets ship at 1280x720 and 820x462, frame edges dissolve through destination-out gradients, and reduced motion shows the final frame. site/src/components/ScrollFilm.tsx, site/src/components/Hero.tsx, site/src/components/AssemblyFilm.tsx

[02]

Pinned course procession and kaiten belt

Above 768px, ScrollTrigger pins the courses section and pans six full-screen spreads sideways, one per menu category, from a literal start value with invalidateOnRefresh so a refresh mid pin cannot freeze the pan; below that the spreads stack. A CSS kaiten belt then loops every plate, or one category's, on a 42-second track that pauses on hover and stops under reduced motion, and any plate adds to the order in one tap. site/src/components/CoursesJourney.tsx, site/src/components/KaitenMenu.tsx

[03]

WhatsApp order with table booking

Dishes reach one persistent order from the course spreads, the belt, the platter cards and the /menu grid, which adds search across names, ingredient lists and categories and a category rail that follows the reader. The order is a module-level store read through useSyncExternalStore, so hydration stays clean and a second tab stays in step, and the drawer composes a prefilled wa.me message with an optional booking: the next fourteen days without Wednesdays, half-hour slots from 13:00 to 22:30, one to twenty guests and notes. site/src/components/CartContext.tsx, site/src/components/OrderDrawer.tsx, site/src/components/OrderMenu.tsx

[04]

Passport, counter scanner and back office

On desktop the passport's cover swings open in 3D as it scrolls into view, revealing a stamp page and a visa page whose QR encodes the member's phone or email. At the counter, the back office scanner reads that QR through the camera, creates or finds the member and offers +1, +2 and -1 stamps and whichever rewards are already reachable. Staff can also search members, pause the programme, change the stamp goal, welcome bonus and card title, edit reward tiers and follow a fourteen-day chart of stamps and rewards. site/src/components/LoyaltyPassport.tsx, admin/src/app/scanner/page.tsx, admin/src/components/MemberPanel.tsx, admin/src/app/scenarios/page.tsx

[05]

Fukubiki prize wheel

The /jeu page draws one of six wedges worth 3, 4, 5, 6, 8 or 10 welcome seals, derived from the reward tiers and weighted towards the wedges just short of the first reward. The server decides the draw from an HMAC of the contact, so a replay lands on the same wedge and there is nothing to police. The SVG wheel drifts at rest, holds a constant-speed spin while the request is in flight, then decelerates forward only onto the server's answer, with a spring-loaded pointer that ticks per wedge. site/src/lib/game.ts, site/src/components/PrizeWheel.tsx, site/src/app/api/game/play/route.ts

[06]

Counter API and wallet pipeline

An Express 4 service on Mongoose 8 exposes 26 endpoints covering admin sign-in, the programme, reward tiers, members, stats, public loyalty reads and wallets. A stamp is one atomic $inc, a redemption checks and deducts the balance in one conditional update so two tills cannot both pay out, and admin routes require a 12-hour JWT. Wallet issuance (HMAC tokens and serials, a passkit-generator .pkpass, an RS256 Google save link, Apple's pass web service with APNs pushes, Google object updates after each stamp) is built on both sides, but the live deployment reports both platforms unconfigured. api/src/routes/members.js, api/src/services/wallet.js, site/src/lib/wallet.ts

Stack
Framework
Next.js 16React 19TypeScript
Motion
GSAP ScrollTriggerGSAP CustomEaseLenis
Loyalty backend
Express 4MongoDBMongoose 8passkit-generatorjsonwebtokenqrcode.reactjsQR
Styling & infra
Tailwind CSS v4DockerNode 22nginx
See it in production

Live in production

Kinben Sushi is running in production today. Open it, then tell us what you want built — BreakNRoot engineers systems like this one from first prototype to production.

Adjacent records
BreakNRoot//SYSTEM.ONLINE