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 :3001This 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 mockAn 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.
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:
pnpm add @openora-addons/<name>- Register its plugin in your
extensions.config.ts- it exports a default plugin. - Merge its contract slice into the contract you pass to
createApp, so OpenAPI and the typed client expose its routes. - 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
- MCP server setup - give your agents the same toolbelt.
- Compliance and audit trail - before real-money flows.