Inside Openora: How our Open AI-Native iGaming Framework for Operators Is Built

Volodymyr Zakhovaiko
Volodymyr Zakhovaiko ·

When we started building our framework, 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 is not 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 platform problem Openora helps operators solve

Every iGaming operator starting from scratch faces the same uphill climb. Before taking a single bet, you need several core systems in place:

  • player accounts, 
  • a wallet, 
  • a game lobby, 
  • bonus campaigns, 
  • KYC checks, 
  • responsible gaming controls, 
  • a backoffice 
  • 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 to offer a third option. That means a solid, complete backend that operators can own and extend around their specific requirements.

Headless - your frontend, your rules

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

We call it headless. The backend handles the state and business logic - including player sessions, wallet operations, game rounds and 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 may run on the same platform while delivering entirely different products.

With a headless backend, one codebase can support both.

What Openora includes from day one 

The framework comes with ten built-in domains. These are not ten unrelated libraries that your team has to connect manually. They are ten areas of the product designed to work together as a coherent system from day one.

Openora includes:

  • identity and authentication,
  • a full wallet with deposit and withdrawal flows,
  • a casino lobby with aggregator integration,
  • a sportsbook with markets and bet placement,
  • player engagement, including chat, notifications and live feeds,
  • a bonus engine,
  • compliance and KYC,
  • a CMS for content,
  • an operator backoffice,
  • an append-only audit trail.

These domains are not simply bolted together. The wallet knows about the bonus engine. The compliance module can block a withdrawal. The audit log records every state change automatically. 

When a player completes a deposit, the event can ripple through the system - triggering bonus checks, notifying the player and being recorded in the audit trail - without the participating modules depending on one another directly.

Plugins - how extension actually works

The plugin system is one of the areas we spent the most time working on. It allows operators to customize the platform 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, such as new routes, adapter bindings or event listeners.

When you want to change something, you create a plugin. 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 reaches a wagering milestone? That can also be handled through a plugin.

The important part is that your plugin lives in your repository, not ours. Your customizations remain separate from the framework core, reducing conflicts when the framework is updated.

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.

This means you can switch PSPs without rewriting your wallet logic. You can run two KYC providers side by side. You can start development with in-memory queues and move to a Redis-backed queue in production.

The business logic remains stable because the underlying implementation is replaceable.

We believe this is how integrations should work. Operators should not be locked into whichever providers the platform vendor happened to select.

Events - modules that don't depend on each other

One of the more difficult architectural decisions was determining how modules should communicate. The temptation is to have the wallet call the bonus engine directly when a deposit arrives. Simple, fast, obvious.

The problem is that direct calls create coupling. The wallet has to know that the bonus engine exists. When one module changes, the other may break. If you later 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 "deposit completed" event and returns to its own work. The bonus engine, 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 does not need to know.

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.

Built for AI

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

Every module ships with a file that explains what it does and how it is structured, written to be useful to both people and AI. The MCP development server exposes the complete API surface in a machine-readable form, including every route, schema, event and integration point. An agent can explore the entire framework without first navigating the entire source code.

Tasks such as scaffolding a new module, adding a route and proposing a database change are handled through structured commands. An agent that understands the framework can do meaningful work on it independently.

We did not add AI support as a separate feature. It changed how we think about what well-documented software should look like.

Why we made Openora open source

Honestly, because we just believe it is the right approach. The infrastructure that powers a gaming platform should not be the operator’s competitive moat. The product, customer experience and business model should be.

Open-sourcing the core raises the technical foundation available to the entire 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 next areas we are developing.

The plugin architecture allows these integrations to be released as optional packages rather than dependencies that 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.

Technical appendix: how it all fits together

The following diagrams show the architecture from different perspectives: the overall system, the internals of individual domains and the deployment topology.

Where Openora ends and your code begins

The framework maintains a clear boundary between what ships as part of the open-source package and what lives in your repository. 

Your code owns the frontend, the plugin configuration, and the app entry point. The OSS core owns the domains, the engine, and the SDK surface.

diagram 1

Inside a domain

Every domain follows the same layered structure. Nothing leaks across layers. The router knows nothing about the database, the service knows nothing about HTTP.

diagram 6

Frontend / Backend split

The frontend never imports server-side code. 

The only bridge is the typed SDK - a lightweight HTTP client generated from the same contracts the backend uses. If a route changes its shape, TypeScript can detect the mismatch on both sides before the change is released.

diagram 4

How modules communicate - event flow

Modules don’t call each other directly. They publish events after committing to the database. Any module that cares subscribes. This is how the system stays decoupled - and how it can later be split into separate services without changing module code.

diagram 2

Plugin Stacking

Plugins load in a defined order. When multiple plugins provide the same token, the last implementation wins. This is how you override a default adapter or extend a built-in domain without touching its code.

diagram 5

Deployment Topology

The same codebase supports three deployment modes without requiring changes to module-level code. Teams can choose the appropriate model based on their scale and operational requirements.

diagram 3

The key guarantee: module code never changes between modes. The seams (event bus, job queue, realtime transport) are adapter-backed. You replace the adapter, not the business logic.

Nothing is hardcoded. Everything is replaceable.

One of the quieter architectural decisions is that the framework does not lock operators into specific infrastructure dependencies.

PostgreSQL, Redis, RabbitMQ and BullMQ may be the defaults, but they are not mandatory.

Every external dependency sits behind an adapter interface. The framework defines what it needs - "something that can enqueue a job", "something that can publish an event", "something that can push a message to a connected client" - and provides a sensible default binding. You can replace that binding by supplying your own implementation through a plugin. 

The job queue defaults to running in-process. When you need durable and retryable background work, you can replace it with BullMQ backed by Redis, Inngest, Trigger.dev or SQS. The wallet service that enqueues the withdrawal job doesn't change. Only the adapter does.

The event bus defaults to in-memory fan-out. It is fast, requires no additional infrastructure and works well when the framework runs as a monolith. When you need events to survive a process restart or cross a service boundary, you bind RabbitMQ - or Kafka, or Redis Streams. One plugin, one environment variable, no module edits.

The realtime transport, which delivers live odds, chat messages and big-win notifications to connected players, defaults to in-process Server-Sent Events. As the framework scales, you can replace it with Ably, Pusher, GetStream or your own WebSocket infrastructure. The frontend hook does not change.

The point is that you don't pay for infrastructure you don't need yet. You can start with the defaults, launch faster and move each layer to a more durable backend when your actual scale requires it. Not on day one because the framework forced you to.

How Openora manages database migrations

 The framework manages its database schema through Drizzle Kit, a code-first migration tool that generates SQL from your table definitions. 

You define a table in TypeScript, run pnpm regen. Drizzle identifies what changed and generates the migration file. You never write raw SQL manually, and you never edit a migration after it's been generated.

The core framework maintains a central migration history covering all built-in domains. When you install a new package version, any new migrations are included with it.

Running pnpm migrate applies whatever hasn't been run yet against your database.The process is additive: it does not drop columns or tables unless the schema definition has been explicitly changed to do so.

Each migration is a plain SQL file committed to the repository alongside the code that depends on it. This means migrations move through the normal Git workflow. They can be reviewed, applied through CI and kept in sync with the application version that generated them.

How operators extend the data model

When you add your own tables - for a custom feature, a plugin, a third-party integration - you define them in your own schema file and run Drizzle Kit separately for your additions. Your migrations remain in your repository, separate from the platform’s migration history. The two sets of migrations run independently and don’t conflict.

If you need to extend an existing platform entity - for example, by adding operator-specific data to a player profile - the recommended approach is to create a separate table in your schema that references the player ID. 

Drizzle enforces a rule that foreign keys exist only between tables within the same module. This prevents your schema from becoming tightly coupled to platform internals that may change. The result is a soft reference: your table, your column and your migration remain under your control.

In practice, framework upgrades and operator-specific schema changes remain independent. You install a new framework version, apply its migrations and then apply your own. The two processes do not interfere with one another.