ARCHITECTURE

Inside Openora: How Our Open AI-Native iGaming Framework Is Built

Volodymyr Zakhovaiko
Volodymyr Zakhovaiko ·

When we started building Openora, we made an early decision that shaped everything: instead of keeping it locked away, we'd open-source the core and let operators build on top of it.

That decision forced us to think carefully about the architecture. An open-source framework isn't just code you share - it's code someone else needs to understand, extend and trust to handle real money. It has to be honest about what it does, and clear about where our code ends and the operator's code begins. This post explains what we built and why we designed it this way.

The problem Openora helps operators solve

Every iGaming operator starting from scratch faces the same uphill climb. Before taking a single bet, you need player accounts, a wallet, a game lobby, bonus campaigns, KYC checks, responsible gaming controls, a backoffice and a complete audit trail.

Most teams end up choosing between two options: spending months, often years, building this infrastructure themselves, or buying a rigid turnkey system they can barely customize. Neither option is ideal - custom builds are expensive and inconsistent, rigid platforms create dependency and limit flexibility. We wanted a third option: a solid, complete backend that operators can own and extend around their specific requirements.

Headless - your frontend, your rules

The first thing people notice is that Openora ships without a user interface. No pre-built pages, no components, no assumptions about what your product should look like. That's intentional.

We call it headless. The backend handles state and business logic - player sessions, wallet operations, game rounds, compliance rules - and exposes them through a typed API. Your frontend team connects through our React SDK and builds the experience your players expect.

This matters more than it might seem. iGaming products can look completely different across markets, brands and player segments - a mobile-first sportsbook in Brazil and a high-roller casino in Malta might run on the same backend while delivering entirely different products. With a headless backend, one codebase can support both.

What Openora includes from day one

The framework comes with nine built-in domains - fourteen modules in total - designed to work together as a coherent system rather than as unrelated libraries you have to wire up yourself: identity and access management, a wallet with deposit and withdrawal flows, a casino domain (game rounds plus a lobby, behind an aggregator port), player engagement (chat, notifications, live feeds over SSE), player account management (profiles, tags, operator notes), compliance and KYC, a CMS for content, an admin console, and an append-only audit trail.

There is no sportsbook and no bonus engine. Those are domains you build on top, and the point of the architecture is that building them shouldn't require touching anything above.

These domains aren't simply bolted together. The compliance module can block a withdrawal. The audit log records every state change. When a player completes a deposit, the event ripples through the system - notifying the player, landing in the audit trail - without the participating modules depending on one another directly.

Your repo, the open-source core, and the infrastructure it talks to - click to zoom in.

Anatomy of a domain

Every domain follows the same internal shape, whether it's the wallet or the lobby. A thin router/ layer parses requests and maps errors. A service/ layer holds all the actual business logic and is the only place that emits events or owns a transaction. A contract/ (Zod schemas, typed routes) is the source of truth for what goes over the wire, and a schema/ (Drizzle table definitions) is the source of truth for what's in the database. plugin.ts just wires these pieces together.

The internal shape of a domain (e.g. Wallet) and how its pieces relate to each other.

Headless - your frontend, your rules

The first thing people notice is that Openora ships without a user interface. No pre-built pages, no components, no assumptions about what your product should look like. That's intentional.

We call it headless. The backend handles state and business logic - player sessions, wallet operations, game rounds, compliance rules - and exposes them through a typed API. Your frontend team connects through our React SDK and builds the experience your players expect.

This matters more than it might seem. iGaming products can look completely different across markets, brands and player segments - a mobile-first sportsbook in Brazil and a high-roller casino in Malta might run on the same backend while delivering entirely different products. With a headless backend, one codebase can support both.

From a hook in your components to a row in Postgres - every hop stays typed.

Events - modules that don't depend on each other

One of the harder architectural decisions was how modules should communicate. The tempting shortcut is to have the wallet call the engagement layer directly when a deposit arrives - simple, fast, obvious. The problem is that direct calls create coupling: the wallet has to know that layer exists, and if you ever want to extract one of them into a separate service, you have to untangle a web of dependencies.

Instead, modules communicate through events. The wallet fires a wallet.deposit.completed event and returns to its own work. The audit module and the engagement layer each decide independently whether and how to respond - all of them may be listening, or none of them may be. The wallet doesn't need to know.

One deposit, one event, subscribers reacting independently - none of them call each other directly. A bonus module, if you build one, subscribes the same way.

Today this all runs in one process. If a module later needs to scale independently, you can connect a message broker without changing the module's code.

Plugins - how extension actually works

The plugin system is the area we spent the most time on. It's how operators customize the framework without forking the core.

Every piece of functionality enters the runtime through a plugin - even the built-in domains. A plugin declares which dependencies must load before it, and what it registers when it starts: new routes, adapter bindings, event listeners. Want to replace the default payment provider with your own PSP? Write a plugin that provides your implementation. Want to add a new API route to the wallet? Use a plugin. Want to trigger a custom campaign when a player hits a wagering milestone? That's a plugin too.

The important part: your plugin lives in your repository, not ours. Your customizations stay separate from the framework core, which means far fewer conflicts when the framework is updated. Plugins load in a defined order, and when multiple plugins provide the same token, the last implementation wins - that's how you override a default adapter or extend a built-in domain without touching its code.

Core plugins load first, your plugins override last-wins, infra plugins swap the defaults.

Adapters - the integration layer

Every third-party integration follows the same pattern. The framework defines an interface - what a payment provider needs to do, what a KYC vendor needs to return - and you supply the implementation.

That means you can switch PSPs without rewriting your wallet logic, run two KYC providers side by side, or start development with in-memory queues and move to a Redis-backed queue in production. The business logic stays stable because the underlying implementation is replaceable. We believe this is how integrations should work - operators shouldn't be locked into whichever providers the framework happened to pick for you.

Nothing is hardcoded, everything is replaceable

The framework doesn't lock operators into specific infrastructure dependencies. PostgreSQL, Redis, RabbitMQ and BullMQ are the defaults, but they're not mandatory. Every external dependency sits behind an adapter interface: the framework defines what it needs - something that can enqueue a job, publish an event, push a message to a connected client - and provides a sensible default binding you can replace through a plugin.

The job queue defaults to running in-process; swap it for BullMQ, Inngest, Trigger.dev or SQS when you need durable, retryable background work. The event bus defaults to in-memory fan-out; bind RabbitMQ, Kafka or Redis Streams when events need to survive a restart or cross a service boundary. The realtime transport defaults to in-process Server-Sent Events; replace it with Ably, Pusher, GetStream or your own WebSocket layer as you scale. In every case, the module code that depends on the adapter doesn't change - only the adapter does. You don't pay for infrastructure you don't need yet.

That also means you're not locked into one deployment shape. Everything starts as a single process with an in-memory event bus and job queue - simplest ops, zero infra overhead. When a subset of domains needs to scale independently, you bind a durable message broker behind the existing MESSAGE_BROKER port and extract the domain into its own service - the events crossing the boundary don't change, only where they run. Today that port has one implementation, the in-process bus; a durable driver is still on the roadmap.

Same code, three deployment shapes - start on mode one, grow into two or three when you actually need to.

How Openora manages database migrations

The framework manages its schema through Drizzle Kit, a code-first migration tool that generates SQL from your table definitions. You define a table in TypeScript, run the regen command, and Drizzle generates the migration file - nobody writes raw SQL by hand, and nobody edits a migration after it's been generated. The core framework maintains a central migration history covering all built-in domains, and any new migrations ship with each package update.

When operators add their own tables - for a custom feature, a plugin, a third-party integration - those migrations live in the operator's own repository, run independently, and never conflict with the framework's migration history. If you need to extend an existing entity, like adding operator-specific data to a player profile, the recommended approach is a separate table that references the player ID: your table, your column, your migration, entirely under your control.

Built for AI

We built Openora during a period when AI-assisted development went from novelty to something our entire team relies on daily - so we designed it to work with AI agents, not just human developers.

Every module ships with an AGENTS.md file explaining what it does and how it's structured, written to be useful to people and AI alike. The MCP development server exposes the complete API surface in machine-readable form - every route, schema, event and integration point - so an agent can explore the framework without first navigating the entire source tree. Scaffolding a new module, adding a route, proposing a database change: all handled through structured commands an agent that understands the framework can run largely on its own.

We didn't add AI support as a separate feature. It changed how we think about what well-documented software should look like in the first place.

Why we made Openora open source

Honestly, because we believe it's the right approach. The infrastructure that powers a gaming product shouldn't be the operator's competitive moat - the product, the customer experience and the business model should be.

Open-sourcing the core raises the technical foundation available to the whole industry. It also makes strategic sense: an open-source foundation means operators aren't dependent on us remaining in business. They can inspect the code, contribute to it and fork it when their requirements demand it. That kind of trust is hard to buy and easy to earn by just being transparent.

What's next

Openora is being actively extended - game aggregators, additional PSP adapters and sports data providers are among the areas we're currently developing. The plugin architecture lets these ship as optional packages rather than dependencies every operator has to carry.

If you're building an iGaming product and want to start from something that's already solved the hard infrastructure problems, Openora is available now. And if you want to contribute - add an adapter, extend a module, improve the docs - the architecture is designed to make that possible without needing to understand the whole codebase first.