Systems

flagship system

Rillex Casino

Full-Stack Crypto Casino Frontend - Next.js 16, 22 Feature Modules, 6 Auth Methods, 20 Languages

I built the frontend for a crypto casino from the ground up. That meant the folder structure and state model, six sign-in methods, EVM and Solana wallet flows, live chat, game discovery, bonuses, referrals, affiliate tools, a blog, SEO pages, and i18n for 20 languages. It talks to a separate backend API while following a dense Figma design at 80-90% fidelity.

scale
22 feature modules, more than 80 API endpoints, 33 WebSocket event types, seven Zustand stores, 20 locales including RTL Arabic, 10 EVM chains plus Solana, six auth methods, six bonus types, 21 transaction types, four banner layouts, and capped lists for chat, live bets, and notifications.
latency
RSC pages use ISR with a 120-second default revalidation window. URL state updates synchronously through replaceState, tokens refresh 60 seconds before expiry, WebSocket retries grow from one to 30 seconds, and search waits 300ms before querying.
reliability
All 33 WebSocket event types map to typed query keys. A shared 401 refresh queue groups concurrent auth failures, TanStack Query deduplicates matching requests, and localStorage uses AES-GCM with PBKDF2-derived keys. TOTP, route guards, BroadcastChannel sync, and automatic cleanup cover the rest of the auth flow.
automation
The product covers game browsing, Quick and Swap deposits, bonus and wagering tracking, referrals, affiliate programs, a blog, and an SEO pipeline with backend-synced metadata, JSON-LD, a dynamic sitemap, and hreflang support.
Rillex Casino home pagefirst proof
The home view with the game catalogue, banners, and live balance.
Context

Problem had shape before code did.

This crypto casino frontend lets users sign in with six methods, connect EVM or Solana wallets, and deposit through Quick mode or Swap mode. Server components prefetch games, banners, and SEO data into dehydrated HTML for the first render. A WebSocket updates balances, notifications, the bet feed, and bonus progress without polling. Users can chat with role-based moderation, browse and favorite games with cursor pagination and infinite scroll, and use the product in 20 languages, including RTL Arabic.

  • Defined the frontend architecture: 22 feature modules with their own components, hooks, queries, services, store, schemas, types, and utilities. I also set the 11-provider order, the URL/Zustand/TanStack Query state split, and shared app.config, images.config, games.config, navigation, and protected-routes files.
  • Implemented six sign-in methods: email, EVM wallet through wagmi, Solana wallet, Passkeys/WebAuthn, X/Twitter OAuth, and Telegram. The system includes BroadcastChannel sync across tabs, active-session revocation, TOTP 2FA, and AES-GCM-encrypted localStorage tokens.
  • Built Socket.IO live chat with locale rooms, user, moderator, admin, and VIP badges, profanity filtering, typed @mention parsing, optimistic messages, local user muting, a three-second rate limit, and a 200-message FIFO Zustand buffer.
  • Integrated wagmi for 10 EVM chains with injected and WalletConnect connectors, plus Solana adapters filtered by signMessage support. Wallet auth requests a nonce, signs it, verifies it, and registers new wallets. Deposits support Quick address and QR confirmation tracking as well as Swap mode.
  • Converted the Figma design into an 80-90% accurate UI using 60+ color tokens, 25+ gradients, and CSS variables for spacing, radii, and shadows. Delivered 20-locale i18n with RTL Arabic, a backend-synced SEO pipeline with JSON-LD and hreflang, and a server-rendered blog with tags, categories, and FAQ schema.
Architecture
The app is divided into 22 feature modules. Shared configuration files keep application-wide settings in one place, while features access each other only through public exports.
State management
Tabs, filters, and modal state live in the URL through replaceState and useSyncExternalStore. Seven Zustand stores hold client state, while TanStack Query owns API data through typed keys per feature.
Provider stack
Eleven providers run in a fixed order: Wagmi, Solana, QueryProvider, Theme, reCAPTCHA, Auth, WebSocket, PlatformConfig, AuthGuard, Dynamic Modals, and LiveChat.
Auth
Users can sign in six ways. BroadcastChannel keeps tabs in sync, AES-GCM protects stored tokens, TOTP adds a second factor, and active sessions can be revoked.
Chains
wagmi supports 10 EVM networks and @solana/kit supports Solana behind one wallet interface.
i18n
The app supports 20 locales, including right-to-left Arabic. Every route has a locale prefix, and `sync-i18n.mjs` keeps translation keys aligned.
Real-time
A native WebSocket carries platform events, while Socket.IO runs chat. Thirty-three event types update only the query data affected by that event.
Layout
The layout has three modes. Mobile uses an SVG bottom navigation and slide-in sidebar, tablet uses an overlay sidebar, and desktop uses fixed sidebars on both sides.
Build

Structure had to survive job.

  • The app is split into 22 feature modules under `src/features/`. Each module keeps its components, hooks, queries, services, store, schemas, types, and utilities together. Auth, web3, games, chat, bonuses, referrals, search, blog, SEO, and notifications all follow that boundary.
  • Shared configuration lives in focused files. `app.config.ts` holds URLs, storage keys, API settings, and SEO defaults; the other files cover images, games, navigation, protected routes, and locale metadata.
  • The provider tree has 11 layers: Wagmi, Solana, QueryProvider, Theme, reCAPTCHA, Auth, WebSocket, PlatformConfig, AuthGuard, Dynamic Modals, and LiveChat. The order matters because WebSocket needs an auth token and Auth relies on QueryProvider for mutations.
  • State has three homes. The URL carries tabs, filters, and modal triggers through `useQueryParam`, Zustand runs seven client-side stores, and TanStack Query owns API data through typed query keys.
  • Browser requests use Axios with a JWT interceptor and a shared 401 refresh queue through `/api/v1/*` rewrites. RSC components use `serverFetch()` directly against the backend with per-route ISR caching and a 120-second default.
  • `CasinoShell` changes its structure across three breakpoints. Mobile uses a fixed header, slide-in sidebar, and Figma-derived SVG bottom nav; tablet uses an overlay sidebar; desktop shows static sidebars. The transitions use a 300ms cubic-bezier curve.
  • Auth, wallet, search, bonus, and wheel modals load through `next/dynamic` with SSR disabled. Zustand keeps the active modal in sync with the `?modal=` URL parameter.
  • Server components prefetch into a `QueryClient` singleton through `React.cache`, dehydrate the result, and pass it through `HydrationBoundary`. The client can use games, banners, and SEO data without requesting them again.
  • `APP_CONFIG` is a module object instead of React context. It holds site, API, socket, storage, WalletConnect, timeout, and SEO settings. `instrumentation.ts` applies backend SEO settings at startup and the client receives them through `window.__APP_SETTINGS__`.
  • Every feature uses the same internal shape: presentational components, feature hooks, typed query and mutation hooks, thin Axios services, an optional store, Zod schemas, and interfaces. Features consume public barrel exports rather than each other's internals.
  • Each feature exposes a query-key factory such as `gamesKeys.list(params)` or `walletKeys.balance()`. The 33 live event types map to those keys, so a deposit confirmation only invalidates the wallet cache and a bonus update only invalidates bonus data.
  • Route protection happens twice. `proxy.ts` checks the session cookie before protected routes render, while `AuthGuard` checks the actual Zustand role on the client. A manually created cookie cannot bypass the client role check.
  • Leaf components receive data through props and do not fetch for themselves. Fetching stays with page components, layout shells, or dedicated `*Content` wrappers.
  • Notifications arrive through WebSocket and enter a Zustand store capped at 50 items. A 30-second unread-count poll acts as a fallback, and users can clear one notification or all of them.
  • Referrals support three levels with category-specific rates in basis points. Affiliates use separate RevShare, CPA, and Hybrid models with applications, approval, period-based GGR calculations, and payout tracking.
  • The app uses `next-intl` with 20 prefix-always locales, so URLs begin with a locale such as `/en/games` or `/ar/games`. Arabic flips the layout for RTL. Locale resolution checks the URL, then a saved cookie, then English. A sync script keeps locale keys aligned.
  • At startup, backend SEO settings merge into `APP_CONFIG`. `generateMetadata()` uses a route override when one exists, otherwise the global default, and adds hreflang alternates for all locales. Games, providers, and categories have their own SEO endpoints, JSON-LD, dynamic sitemap entries, and configurable robots rules.
This frontend has six auth flows, more than 10 chains, moderated live chat, 20 locales including RTL Arabic, live balances, games, bonuses, referrals, affiliates, and a separate backend. Feature modules keep that work close to the feature that owns it. The three state tiers give URL state, client state, and API state separate homes. Shared config files keep settings discoverable, and Figma tokens make visual changes local. The main event channel uses native WebSocket because the Bun backend already supports it; chat uses Socket.IO because rooms and reconnection fit that job.
Working stack

Next.js 16 (App Router, RSC, React Compiler) / TypeScript 5 (strict) / Bun 1.3 / Tailwind CSS v4 / shadcn/ui (base-maia variant) / Zustand 5 / TanStack Query 5 / Axios / wagmi 3 + ethers 6 (EVM, 10 chains) / @solana/kit 6 + @solana/react 6 (Solana) / next-intl 4 (20 locales) / Socket.IO (live chat) / Native WebSocket (event system) / Zod 4 / react-hook-form 7 / Framer Motion 12 / Recharts 3 / Google reCAPTCHA v3 / Web Crypto API (AES-GCM)

Evidence

Browser becomes part of argument.

Evidence viewerKeyboard arrows change proof

01 / 15The slots catalogue with infinite scroll, category filters, and saved favourites.

Decisions

Small cuts make system trustworthy.

  • Email sign-in validates with Zod, receives a reCAPTCHA v3 token, posts to `/auth/login`, then asks for TOTP when needed. Wallet sign-in connects through wagmi or Solana, requests a nonce, signs it, verifies the signature, and creates a username for a new wallet. Passkeys use WebAuthn, X/Twitter uses OAuth, and Telegram uses its Login Widget.
  • A `BroadcastChannel` named `rillex-auth` keeps open tabs in step. After login, each tab receives the token and user data, stores its encrypted copy, and schedules its own refresh. Logout broadcasts cleanup so every tab clears state, encrypted localStorage, and the session cookie.
  • Users can inspect active sessions by browser, OS, device type, IP address, and last activity. Revoking any session, including the current one, performs a full logout.
  • Chat uses Socket.IO separately from the main WebSocket and joins people to locale rooms with history. A message passes through the three-second rate limit, profanity filter, typed `@mention` parser, optimistic store insert, and server emit. Confirmation clears its pending flag; failure removes it. The store keeps 200 messages and local muting persists in localStorage.
  • The game catalog uses cursor pagination and infinite scroll with category, provider, sub-provider, and tag filters. Favorite buttons update immediately, then roll back if the request fails. Launching a game posts for a session URL and renders it in an iframe.
  • Global search opens by click or `⌘K`. It waits 300ms after input, needs two characters, keeps results fresh for 30 seconds, and shows matching game cards from the games endpoint.
  • Blog pages prefetch through `serverFetch()` and ISR so the first render includes HTML. Posts have tags, categories, related content, FAQ JSON-LD, and paginated listings.
  • Quick deposits post a chain and currency to `/payments/deposit/address`, show the returned address and QR code, then follow WebSocket events from pending through confirmation to balance update. Swap mode converts an existing asset before it reaches the user's balance.
  • The main WebSocket is a custom native client. It connects with the JWT query parameter, pings every 30 seconds, and retries with increasing delays from one to 30 seconds. Auth close codes 4001 through 4003 do not reconnect. `updateToken()` swaps a refreshed token without disconnecting.
  • The access token and user object in localStorage use AES-GCM through the Web Crypto API with a PBKDF2-derived key. Middleware reads only the `rillex-auth-session=1` marker, not the token itself.
Pressure

What resisted, broke, stayed expensive.

Constraints in motion

  • The hard part was coordination. Six auth flows, cross-tab sessions, moderated chat, 10 EVM chains plus Solana, infinite-scroll discovery, optimistic favorites, 20 locales including RTL Arabic, and a dense Figma file all had to behave as one product.
  • A login or token refresh in one tab had to reach the other tabs without incorrectly logging one of them out.
  • Chat needed optimistic messages, profanity filtering, mention parsing, role badges, and rate limiting in the browser at the same time.
  • Wallet work had to support 10 EVM chains and Solana, two deposit modes, and live status changes from WebSocket events.
  • The Figma file contained dozens of distinct colors and gradients. I had to extract them into CSS variables without flattening the visual character of the interface.

Failure modes

  • If the backend is unavailable during startup, `syncAppConfig()` falls back to hardcoded SEO defaults. Pages still render, but metadata can remain stale until the next restart.
  • A burst of 401 responses can grow the refresh queue while a token refresh is in progress. `clearAuth()` stops new authenticated requests, but requests already in flight can still join the queue.
  • BroadcastChannel works only between same-origin tabs. Incognito windows, other browsers, and other devices do not receive the auth update, so sessions can diverge.
  • The chat store keeps 200 messages. Earlier messages disappear because it has no paginated history beyond the history received when a room opens.
  • If AES-GCM decryption fails because storage is corrupted or the browser profile has different keys, `initAuth()` calls `clearAuth()`. The user is logged out without a specific explanation.
  • A favorite can briefly show the wrong state when the network request fails after the optimistic toggle. The cache rolls back, but the change is visible.

Trade

  • The main event channel uses native WebSocket instead of Socket.IO because the Bun backend supports native WebSocket. Chat still uses Socket.IO because it benefits from rooms and built-in reconnection.
  • Access tokens stay in AES-GCM-encrypted localStorage because the Axios interceptor and WebSocket need JavaScript access. The refresh token stays in an HttpOnly cookie. This keeps a token in localStorage, but encryption raises the cost of exploiting it through XSS.
  • URL query state uses replaceState and useSyncExternalStore instead of Next.js router methods. Tab, filter, and modal changes stay immediate, but Next.js route events do not see them.
  • APP_CONFIG is a plain module export, not a React context. It is importable without provider nesting but does not trigger rerenders when it changes. That is acceptable because it changes only at startup.
  • Feature modules own their types, services, queries, and optional store. Some types repeat across features, but modules stay independent.
  • Chat caps its Zustand store at 200 messages. Long sessions lose earlier messages without scrollback in exchange for avoiding paginated history in short interactions.
  • A 1.5-second retry window handles cross-tab token-refresh timing. If one tab invalidates the old refresh cookie, another tab can receive the new token through BroadcastChannel before it gives up.
Result

What held. What carries forward.

architecture

22 feature modules with explicit boundaries

Each module owns its components, hooks, queries, services, store, schemas, and types. Features consume barrel exports rather than each other's internals.

behavior

Cross-tab auth sync handles refresh timing

The `rillex-auth` BroadcastChannel carries login, logout, and token refresh messages between open tabs. A 1.5-second grace period gives a second tab time to receive a refreshed token.

metric

80-90% Figma fidelity through a token system

More than 60 color tokens, 25 gradients, and the spacing, radius, and shadow values are CSS custom properties. A visual change can usually happen at the token instead of across many components.

architecture

Real-time balance, bet feed, and bonus updates

A native WebSocket manager uses a JWT query parameter, a 30-second heartbeat, retry delays from one to 30 seconds, and token replacement without disconnecting. Its 33 event types invalidate only the affected query keys.

If rebuilt

  • Add server-loaded, paginated chat history instead of relying on the 200-message FIFO cap.
  • Add a server-side session registry for sync across incognito windows, browsers, and devices.
  • Put an explicit bound on the 401 refresh queue rather than relying on `clearAuth()` to stop new requests.
  • Show a clear recovery state when AES-GCM decryption fails instead of silently logging the user out.
  • Move large filtered game results to server-driven pagination to reduce client payload.
結論

Upfront structure reduced friction as features accumulated. The feature-module pattern made locations for types, queries, and stores predictable. APP_CONFIG kept shared URLs and storage keys discoverable, and the URL/Zustand/TanStack Query split gave each kind of state a single owner. Defining design tokens from Figma before building components also turned visual updates into CSS variable changes instead of repeated edits.

Make it hold together

Complex frontend system need steady hand?

Architecture, interface, production constraints. Together.