DocumentationConsuming the Platform
DOCS

Consuming the Platform

How an operator builds their own iGaming on top of @openora/core without forking: scaffold, compose a contract, seed reference data, extend with overlays.

Introduction

How a downstream operator builds their own iGaming on top of @openora/core without forking core. This is the detail you load when you are actually wiring a consumer.

Fastest path: scaffold the repo

From an Openora checkout, generate a consumer turborepo:

pnpm create:app ../my-igaming --name my-igaming
cd ../my-igaming
pnpm install
pnpm setup:mcp           # trust the MCP server + install the /start onboarding flow
cp .env.example .env     # set DATABASE_URL + AUTH_SECRET
pnpm db:migrate          # apply the core schema
pnpm dev                 # api on :3001

This emits a headless API consumer: a thin createApp entry plus extensions.config.ts, .mcp.json, the consumer AI agents, and turbo/generators/ exposing pnpm gen plugin and pnpm gen adapter.

The platform is headless backend only. Build your entire frontend - player web, admin backoffice, components, styling, theme - in your own repo, and consume the API over HTTP via @openora/core/react.

API entrypoint

A consumer imports @openora/core/server and @openora/core/contracts and creates an API instance. createApp is domain-agnostic and single-tenant: the consumer composes its own contract and injects its identity tables.

// my-igaming/apps/api/src/main.ts
import { createApp } from "@openora/core/server";
import { composeContract } from "@openora/core/contracts";
import {
  user,
  session,
  account,
  verification,
  twoFactor,
} from "@openora/core/pam/schema/identity";
import { identityContract } from "@openora/core/pam/contracts/identity";
import { walletContract } from "@openora/core/wallet/contract";
import { extensions } from "./extensions.config.js";
 
// Compose only the modules you enable - composeContract adds `health` itself.
const contract = composeContract({
  identity: identityContract,
  wallet: walletContract,
});
 
const { listen, emitOpenApiSpec } = await createApp({
  plugins: extensions,
  contract,
  authSchema: { user, session, account, verification, twoFactor },
  port: 3001,
  cors: { origins: ["https://my-igaming.example"] },
  openapi: { info: { title: "my-igaming API", version: "1.0.0" } },
});
 
await listen();
await emitOpenApiSpec();

Extending without forking

Everything you add is an overlay under extensions/<name>/, registered in your own extensions.config.ts:

pnpm gen plugin loyalty    # a new feature: routes, event handlers, job workers
pnpm gen adapter           # bind a real vendor over a shipped mock

An overlay loads after the core modules, so ctx.provide() on an existing adapter token rebinds it - last-wins. That is how you replace the mock PSP with your real one, without touching a line of wallet code.

Sealed tokens are the exception. A regulator-mandated service is bound once, by its owning module, and an overlay attempting to rebind it throws at boot.

Seeding reference data

Migrations carry structure only - tables, indexes, constraints - never data. Reference data a module needs to function, such as IAM's predefined backoffice roles, is seeded separately by module seeders: a function each module exports from its /seed subpath.

Seeders are convergent upserts, so editing the declared data and re-running reconciles existing rows. They are safe to run on every deploy.

// my-igaming/apps/api/src/seed.ts - run after db:migrate
import { createDrizzleDb } from "@openora/core/server";
import { seedRoles } from "@openora/core/iam/seed";
 
const db = createDrizzleDb(process.env.DATABASE_URL!);
 
await seedRoles(db);
console.log("Reference data seeded.");

Seeding is a standalone one-shot script. It needs only a database connection - it never boots the app, so there is no HTTP, no auth, and no plugin host in the running server's footprint. Each seeder is a plain (db) => Promise<void>, so you can run any one in isolation, in any order.

Demo data - sample players, transactions - is a separate, dev-only concern. It lives in @openora/testing and must never run on production.

Building the frontend

Your frontend consumes the API over HTTP through @openora/core/react: typed client, data hooks, auth, realtime transport. Wrap your root layout with ApiClientProvider:

// your-frontend/app/providers.tsx (client component)
import { ApiClientProvider } from "@openora/core/react";
import "./globals.css";
 
<ApiClientProvider client={{ baseUrl }}>{children}</ApiClientProvider>;

The platform ships no UI. Pick whatever you like - Tailwind, MUI, your own design system. Openora only feeds you data through the hooks.

Import Drizzle from the framework, not directly

A consumer's own Drizzle code must import tables and operators from @openora/core/server/orm, not from drizzle-orm. A direct import resolves to your own physical copy, and drizzle's protected-member classes then fail nominal type checks against DrizzleService.db.

Premium add-ons

@openora/core ships the fourteen core modules plus one gated add-on. Premium add-ons will be distributed as separate packages. When available, you enable one the same way:

  1. pnpm add @openora-addons/<name>
  2. Register its plugin in your extensions.config.ts - it exports a default plugin.
  3. Merge its contract slice into the contract you pass to createApp, so OpenAPI and the typed client expose its routes.
  4. If the package owns tables, run its migrations after the core set.

The core build never references add-on packages - a lint boundary, no-core-to-addon, enforces it - so you only ever pull in what you enable.

Next

Updated 1 day ago
Consuming the Platform | Openora.ai