Systems

major system

MemeWin - Web3 Crypto Casino Platform

Large-Scale Refactor - 54+ Providers, 9 EVM Chains, 8-Branch Zustand Tree, Runtime Theming, ~75K Lines

I inherited a 75K-line crypto casino with 1,500-line transaction hooks and inconsistent boundaries. The refactor split that work into 21 focused hooks, a 32-slice Zustand tree, six singleton services, an OKLCh theme system with 22 variants, and a backend-driven SEO setup using Prisma and JSON-LD. The work happened while memewin.bet remained in production.

scale
The codebase contains 75,035 lines of TypeScript across 456 source files. It includes 194 React components, 57 custom hooks, 32 store slices, six singleton services covering 2,966 lines, 26 type-definition files, 20 locales, more than 54 game providers, nine EVM chains, and 11 themes with dark variants.
latency
WebSocket retries wait two seconds times the attempt number, up to five attempts and a maximum ten-second delay. Token conversion data has a two-minute TTL, game lists use ten-minute stale-while-revalidate caching, and localStorage hydrates on startup.
reliability
Fetch-capable store slices use a setter, async worker, and guard so they can avoid duplicate requests. Token fetching retries up to five times. Separate WebSocket state machines, a 401 refresh queue, and BroadcastChannel logout keep live state and sessions predictable.
automation
Route navigation uses an optimistic client cache. Dynamic imports split the homepage carousels, betting table, and blog slider into separate chunks. `sync-translations.js` uses the OpenAI API to fill missing i18n keys across 20 locales.
Context

Problem had shape before code did.

memewin.bet is a crypto casino that aggregates 54+ game providers across nine EVM chains. Users connect EVM wallets through Dynamic Labs, deposit, withdraw, and swap on-chain with WebSocket transaction updates, and launch games in iframes. They can choose from 11 themes and custom fonts. A separate admin system manages blog posts and SEO landing pages.

  • Split three monolithic transaction hooks of about 1,500 lines each into 21 focused hooks covering form state, gas estimation, allowance checks, quote fetching, and transaction execution.
  • Designed a leaf, branch, and tree Zustand store with 32 slices across eight top-level branches. It uses Immer and reselect memoized selector chains across 4,522 lines of store logic.
  • Built six class-based singleton services totaling 2,966 lines, including ApiService, WebSocketService, TransactionService, and LocalStorageService.
  • Created 11 color themes with light and dark variants in 2,365 lines of Tailwind v4 CSS using OKLCh. Users can switch themes and fonts without a page reload.
  • Moved SEO from duplicated meta tags into a centralized backend-driven system. Server actions load per-page Prisma data, the schema generator creates JSON-LD, and SSR renders metadata on first paint.
Architecture
The SPA talks to an external API and nine EVM chains. A separate Next.js server runs the admin CMS without connecting to the casino backend.
State
Zustand follows a leaf, branch, and tree structure: 32 slices become eight branches, using Immer and reselect across 4,522 lines of memoized selector logic.
Services
Six class-based singletons cover 2,966 lines: ApiService, a dual Socket.IO WebSocketService, a 1,068-line TransactionService, and LocalStorageService among them.
Theming
Eleven themes with light and dark modes create 22 OKLCh CSS rule sets across 2,365 lines, with Google Fonts and custom font uploads.
i18n
The app supports 20 locales. It resolves a locale from the cookie, then localStorage, then the browser, while `sync-translations.js` fills missing translations through the OpenAI API.
WS
Transaction and chat use separate Socket.IO connections with independent reconnection state machines. Transaction updates require user authentication; chat does not.
SEO
Prisma, JSON-LD, and server-rendered metadata support backend-managed SEO. The admin can update landing pages without a frontend deployment.
Auth
The 324-line Dynamic Labs hook was split into four modular hooks of 138, 83, 48, and 241 lines.
Build
Production removes console output except error, warn, and dir. It ships without sourcemaps, optimises package imports, uses WebP assets, and dynamically loads client-only sections.
Build

Structure had to survive job.

  • The browser app uses React 19, Zustand, and a service layer. It talks to a POST-only external API and two Socket.IO servers, while on-chain actions use wagmi and viem across nine EVM chains. A separate Next.js server runs the admin CMS with its own Prisma and MySQL connection.
  • Zustand state moves from atomic slice files such as `network.slice.ts` and `token.slice.ts`, into branch aggregators such as `blockchain/index.ts`, then into the root `AppStore`. A standalone `transactionStore` keeps paginated deposit and withdrawal history on its own lifecycle.
  • The route layout nests DynamicContextProvider, WagmiProvider with nine chain transports, AuthProvider, and SidebarProvider. Four effect-only synchronizers bridge wallet, auth, transaction, and WebSocket changes into the store.
  • Middleware handles admin JWT auth, locale routing, and SEO redirects in that order. It decrypts `blog_session`, redirects an invalid admin to `/admin/login`, reads the locale prefix into the cookie and header, then redirects legacy query URLs to clean slugs.
  • Game routes such as `/games/pg-soft/slot` map to a provider and category, update the Zustand query slice, then flow through memoized selectors from filtered games to sorted and visible games. Keyword expansion lets a lottery search include bingo, keno, and lotto.
  • The admin CMS has its own JWT flow, Prisma models for User, Post, Tag, and SeoPage, and server actions for CRUD with ISR revalidation. It does not share a data path with the external casino backend.
The original 1,500-line hooks mixed form state, gas estimation, API calls, and transaction execution, which made a failed transaction hard to trace. The two WebSocket connections needed their own retry state, and the theme system needed 22 rule sets without a flash during a switch. The leaf, branch, and tree Zustand structure gave the 75K-line codebase smaller state boundaries. Each change answered a specific problem in the inherited code.
Working stack

Next.js 15 (App Router, Turbopack) / React 19 / TypeScript (strict) / Tailwind CSS v4 (OKLCh color space) / Zustand 5 + Immer + DevTools / reselect (memoized selectors) / Wagmi 2.15 + viem 2.31 + ethers 6 / Dynamic Labs SDK 4.25 (Web3 auth) / Socket.IO (dual connections) / Prisma 5 + MySQL / jose + bcryptjs (admin JWT auth) / next-intl (20 locales) / DiceBear (avatar generation) / Farcaster miniapp-sdk

Evidence

Browser becomes part of argument.

Evidence viewerKeyboard arrows change proof
Production trace
WS: transaction connected - userId: 12345
Deposit flow: `approve()` confirmed, `transfer()` sent, the API recorded it, then the transaction WebSocket reported the status.
Theme: 'midnight-ocean' applied - CSS vars swapped, zero flash (class injected before hydration)
Search: `pg soft` ran through `selectFilteredGames` and `selectVisibleGames`, then used the memoized selector chain.
Cross-tab logout: received the broadcast, cleared Zustand, and removed the cookie.

01 / 01Production trace

Decisions

Small cuts make system trustworthy.

  • Three transaction hooks of roughly 1,500 lines became 21 focused hooks. Deposit has six hooks, withdrawal has five, swap has seven, and tip has three, covering form state, calculations, gas, network checks, transactions, quotes, and orchestration across 4,845 lines.
  • The service layer uses explicit singletons. `ApiService` has more than 25 endpoints, `WebSocketService` runs two Socket.IO state machines with up to five retries, `TransactionService` handles the ERC-20 approve, transfer, and API record flow, and `LocalStorageService` provides typed storage with TTLs.
  • Fetch-capable slices use three steps: a setter for hydration and tests, an async worker that calls `ApiService`, and a guard that checks cache age and in-flight work. The token slice retries failed fetches up to five times.
  • A banner factory maps a layout key to a `PropBuilder` that calculates layout-specific props. The dropdown can switch among nine layouts at runtime, including Layout1 through Layout8 and LayoutX.
  • The theme engine defines about 35 OKLCh custom properties for each of 11 named themes and their dark overrides in 2,365 lines of CSS. `theme-toggle.tsx` swaps the theme at runtime, and a three-component font system supports Google Fonts and uploaded `.ttf` or `.otf` files through blob URLs.
  • The global search modal looks across providers, games, categories, recent items, and favorites. A `favorites:changed` CustomEvent keeps separate components in sync without coupling them to a store. BroadcastChannel clears auth state across tabs after logout.
  • The Dynamic Labs integration became smaller hooks: `useAuthStatus`, `useUserData`, `useWalletContext`, and `useDynamicAuthRefactored`. Together they replace the original 324-line `useDynamicAuth` hook.
Pressure

What resisted, broke, stayed expensive.

Constraints in motion

  • The inherited 1,500-line hooks mixed form state, gas estimation, API calls, and transaction execution, so a transaction failure was difficult to isolate.
  • An on-chain transaction moves through approve, transfer, and API recording. Insufficient gas, a user rejection, an RPC timeout, or a backend rejection can stop it at different points, so the UI needs clear state across each async boundary.
  • Transaction updates and chat use different WebSocket connections. Each needed retry logic that could recover without holding up the other.
  • The theme system needs 22 OKLCh rule sets and a clean switch. Tailwind v4 JIT initially produced a frame of unstyled content during the change.
  • SEO was spread across duplicated files. The refactor had to move it to the backend while retaining server-rendered metadata for search engines.

Failure modes

  • During the hook split, a bad argument path between `useDepositCalculations` and `useDepositTransaction` left gas estimation on an old token amount. Tracing the value across six hook boundaries exposed the issue.
  • `approve()` could succeed while the backend still read an allowance that had not reached its node. Waiting for confirmation between approve and transfer removed that race.
  • When the backend was down, both WebSocket connections retried on the same schedule. Staggered delays and independent state machines stopped that burst of retry traffic.
  • Tailwind v4 JIT caused one unstyled frame during a theme change on route navigation. Applying the theme class in the root layout before hydration removed the flash.
  • A search for `lot` partly triggered the `lottery` expansion and returned bingo or keno for an unrelated query. Expansion now requires an exact trigger word.

Trade

  • The external backend accepts only POST requests with `api_key` in the request body. That rules out standard REST and CDN edge caching, but it matches the backend contract.
  • localStorage is the main cache because the Next.js frontend has no persistent server state and the external backend does not support ETags or If-Modified-Since. TTL-based invalidation keeps that cache bounded.
  • Transactions and chat use separate Socket.IO connections because their authentication needs differ and the backend exposes separate servers for them.
  • Class-based singletons make lifecycle actions such as destroy and reconnect explicit and work with DevTools, though they require more setup than module-scoped instances.
  • Zustand and Immer keep the store lightweight and allow direct mutation syntax. The tradeoff is manually maintaining the setter, worker, and guard pattern that RTK Query would provide.
  • OKLCh gives more consistent colour adjustments across themes, but requires browsers that support the `oklch()` function. There is no fallback for IE11 or older Safari.
Result

What held. What carries forward.

architecture

Three monolithic hooks became 21 focused modules

Deposit uses six hooks, withdrawal five, swap seven, and tip three. Together, the 21 hooks span 4,845 lines and can be debugged independently.

architecture

An eight-branch Zustand store keeps state work divided

Thirty-two slices feed eight branches through Immer and reselect across 4,522 lines. The `selectVisibleGames` chain alone contains 360 lines of memoized selectors with keyword expansion.

metric

Eleven OKLCh themes switch at runtime without a flash

The theme system includes 2,365 lines in `globals.css`, an 883-line `theme-toggle.tsx`, and a 569-line font system. Applying the theme class before hydration prevents Tailwind v4 JIT from flashing unstyled content.

architecture

SEO is centrally managed without frontend deployments

`global-seo.config.json`, `seo-config-loader.ts`, and `schema-generator.ts` supply JSON-LD for Organization, Website, BreadcrumbList, and Article. The admin CMS uses ISR revalidation.

If rebuilt

  • Add an event-driven layer for scaling beyond one server.
  • Add replay tooling to test production execution paths against recorded events.
  • Add circuit breakers so one failing dependency does not cascade through the product.
  • Add server-loaded, paginated chat history.
  • Add a server-side session registry for multi-device auth sync beyond BroadcastChannel.
  • Add a client recovery path for interrupted LI.FI swaps, backed by MX or the LI.FI explorer.
結論

Breaking stateful transaction logic into focused hooks made failures easier to isolate. The service layer provided explicit connect, disconnect, and retry lifecycles across consumers. The leaf, branch, and tree store structure kept selector and state boundaries clear as the frontend grew.

Make it hold together

Complex frontend system need steady hand?

Architecture, interface, production constraints. Together.