Building a single game is fun. But building an entire arcade portal—a fast, responsive platform that hosts dozens of games, tracks high scores, offers creative tools, and monetizes cleanly—requires deliberate architectural discipline.
If you make the mistake of importing raw game loops directly into your React components, your site will quickly grind to a halt. Game loops will fight React state reconciliation, WebGL memory leaks will persist across page transitions, and a single crash in one game will tear down your entire application.
Here is the exact Isolated Monorepo Architecture that powers ToozyMio in production.
1. The Core Architecture: The Isolation Rule
toozymio/
├── src/ # Main Portal (Next.js 16 + Tailwind CSS)
│ ├── app/ # App Router (SSG / SSR pages)
│ ├── config/games.ts # Game catalog registry & metadata
│ ├── components/Game/ # Iframe player & PostMessage PlayBridge
│ ├── services/leaderboard.ts # Cloud Firestore Leaderboard API
│ └── content/ # Devlogs, guides, and tutorials
├── apps/ # Individual Game Applications
│ ├── tiny-ocean-tank/ # Vite + Canvas / React
│ ├── cosmic-chaos/ # Vite + Three.js
│ └── rift-runner-infinite/ # Vite + Canvas
├── public/ # Static assets & game bundles
│ ├── games/ # Covers, screenshots, audio
│ └── games-app/ # Compiled Vite index.html builds
└── scripts/
├── create-game.sh # One-command scaffolding CLI
└── sync_assets_to_s3.sh # CloudFront CDN synchronization
The Golden Rule: Strictly Isolate Games in Iframes
- The Next.js website and the games never import each other's code.
- Games are built as lightweight static bundles (
apps/<slug>/dist/) using Vite and copied intopublic/games-app/<slug>/. - The Next.js portal renders games inside an
<iframe src="/games-app/<slug>/index.html" />.
Why This Architecture Wins:
- Zero Memory Leaks: When a player clicks "Exit" or navigates back to the catalog, the iframe is unmounted. The browser engine immediately garbage-collects all Canvas textures, WebGL buffers, and Web Audio nodes.
- Framework Freedom: Game 1 can be React + Pixi.js, Game 2 can be pure Canvas, and Game 3 can be Three.js or Phaser.
- Build Resilience: The root
tsconfig.jsonintentionally excludesapps/**. A TypeScript error in an experimental game never blocks or breaks the main website build.
2. The PlayBridge Contract: Typed PostMessage Synchronization
How do the Next.js parent portal and the sandboxed iframe game communicate safely across window boundaries? Through a secure, typed window.postMessage bridge:
sequenceDiagram
participant NextPortal as Next.js Parent Portal
participant GameApp as Sandboxed Iframe Game
GameApp->>NextPortal: { type: 'TOOTH_READY', slug: 'cosmic-chaos' }
NextPortal-->>GameApp: { type: 'TOOTH_LOAD_STATE', state: { ... } }
Note over GameApp: Player beats high score
GameApp->>NextPortal: { type: 'TOOTH_SCORE', score: 2450 }
Note over NextPortal: Writes to Cloud Firestore Leaderboard
GameApp->>NextPortal: { type: 'TOOTH_EXIT' }
NextPortal->>NextPortal: router.push('/games/cosmic-chaos')
What the Game Sends:
// Inside apps/<slug>/src/toozymio-sdk.ts
export const ToozyBridge = {
init: (config: { slug: string }) => {
window.parent.postMessage({ type: 'TOOTH_READY', slug: config.slug }, '*');
},
submitScore: (score: number) => {
window.parent.postMessage({ type: 'TOOTH_SCORE', score }, '*');
},
exitGame: () => {
window.parent.postMessage({ type: 'TOOTH_EXIT' }, '*');
}
};
What Next.js Listens For:
// Inside src/components/Game/PlayBridge.tsx
useEffect(() => {
function handleMessage(event: MessageEvent) {
if (event.data?.type === 'TOOTH_SCORE') {
// Auto-submit to Cloud Firestore Leaderboard!
submitLeaderboardScore(gameSlug, playerName, event.data.score);
}
if (event.data?.type === 'TOOTH_EXIT') {
router.push(`/games/${gameSlug}`);
}
}
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [gameSlug, playerName]);
3. High-Performance CDN Delivery: S3 + CloudFront
Web games often feature dozens of sound effects, background ambient loops, and sprite sheets. Hosting hundreds of megabytes of media inside a serverless container or Git repository degrades build speeds and costs money.
Our Dynamic Asset Resolution Layer
In src/utils/cdn.ts, we implement environment-aware asset resolution:
export const DEFAULT_CDN_BASE = 'https://cdn2.toozymio.com/public';
export function cdnUrl(path: string | undefined | null): string {
if (!path) return '';
if (/^(?:https?:)?\/\//i.test(path) || path.startsWith('data:')) {
return path;
}
let cleanPath = path.startsWith('/') ? path : `/${path}`;
if (cleanPath.startsWith('/public/')) cleanPath = cleanPath.slice(7);
// In development: serve from local filesystem
const forceCdn = process.env.NEXT_PUBLIC_USE_CDN === 'true';
const isProd = process.env.NODE_ENV === 'production';
if (!forceCdn && !isProd) return cleanPath;
// In staging / production: serve from fast CloudFront edge cache
return `${DEFAULT_CDN_BASE}${cleanPath}`;
}
Assets are synced to S3 with immutable caching headers:
aws s3 sync public/ s3://toozymio-cdn-assets/public/ \
--cache-control "public, max-age=31536000, immutable"
Once a player visits ToozyMio, media assets load from local browser cache in under 20 milliseconds.
4. SEO & Structured Data for Games: VideoGame JSON-LD
To rank on Google for terms like "play free browser games", every game page must serve rich structured data:
// Inside src/app/games/[slug]/page.tsx
const jsonLd = {
"@context": "https://schema.org",
"@type": "VideoGame",
"name": game.title,
"description": game.description,
"genre": game.genre,
"playMode": "SinglePlayer",
"applicationCategory": "Game",
"operatingSystem": "Any",
"image": cdnUrl(game.coverImage),
"url": `https://toozymio.com/games/${game.slug}/play`
};
This ensures Google displays rich snippets, star ratings, and game badges in search results.
5. Instant Game Scaffolding via CLI
To keep our catalog growing, we wrote an automated scaffolding script that creates a new fully compliant game in 3 seconds:
npm run new-game neon-pinball "Neon Pinball" "Arcade kinetic pinball simulation" "Arcade"
This single command:
- Copies the
templates/game-starterboilerplate toapps/neon-pinball/. - Registers the game in
src/config/games.tswith metadata, tags, and theme gradients. - Configures Vite with relative base hosting (
base: './').
Conclusion: Ship Faster with Boundaries
Architectural boundaries give you creative freedom. Because our games are completely decoupled from our portal, we can experiment with wild new physics engines, shaders, and mechanics without ever risking the stability of ToozyMio.
Browse our catalog of games in the Games Catalog, or check your ranking on our global Leaderboard!




