Skip to content
4 changes: 2 additions & 2 deletions .rulesync/commands/release-plugin.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
targets:
- '*'
description: 'Validate an overlay plugin against the definePlugin contract, build it, and optionally publish to npm. Arg: path to the plugin directory (eg extensions/my-plugin).'
description: 'Validate an overlay plugin object, build it, and optionally publish to npm. Arg: path to the plugin directory (eg extensions/my-plugin).'
---

Given the plugin path from $ARGUMENTS:

1. Read `plugin.ts` - it must export `definePlugin({ id, register })`.
1. Read `plugin.ts` - it must default-export `{ id, register } satisfies Plugin<CoreTokenCatalog>`.
2. `pnpm verify --filter <plugin-package-name>` - types and tests pass.
3. Check `AGENTS.md` exists and is filled in (not the template).
4. `pnpm -F <plugin-package-name> build`.
Expand Down
2 changes: 1 addition & 1 deletion .rulesync/rules/messaging-and-microservices.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ The `EventBus` wraps every emission at the broker boundary; module code never bu
- `orderingKey` - Kafka partition key / RabbitMQ routing for per-user ordering.
- `schemaVersion` - forward-compatible payload evolution. `traceId` - correlation.

Because the envelope isolates transport from domain logic, binding a durable broker is an overlay swap (a `definePlugin` re-providing `MESSAGE_BROKER`) and extracting a module needs no module edits. Migration path: Redis Streams (default, `REDIS_URL`) -> RabbitMQ/Kafka (a consumer overlay implementing `MessageBrokerAdapter`); `topic` maps to routing key/topic, `orderingKey` to partition key, `consumerGroup` to durable queue/consumer group, `eventId` to dedup. Swap when you need what Streams lacks: partitioned ordering (`orderingKey` is not honoured), unbounded retention (Streams trim at `STREAM_MAXLEN`), or a dead-letter queue. `AMQP_URL`/`RABBITMQ_URL` do NOT bind a broker - core ships no AMQP driver; they only enable the transactional outbox, same as `OUTBOX_ENABLED`.
Because the envelope isolates transport from domain logic, binding a durable broker is an overlay swap (a plugin object re-providing `MESSAGE_BROKER`) and extracting a module needs no module edits. Migration path: Redis Streams (default, `REDIS_URL`) -> RabbitMQ/Kafka (a consumer overlay implementing `MessageBrokerAdapter`); `topic` maps to routing key/topic, `orderingKey` to partition key, `consumerGroup` to durable queue/consumer group, `eventId` to dedup. Swap when you need what Streams lacks: partitioned ordering (`orderingKey` is not honoured), unbounded retention (Streams trim at `STREAM_MAXLEN`), or a dead-letter queue. `AMQP_URL`/`RABBITMQ_URL` do NOT bind a broker - core ships no AMQP driver; they only enable the transactional outbox, same as `OUTBOX_ENABLED`.

## Deployable topology - the service manifest (ADR-0017)

Expand Down
4 changes: 2 additions & 2 deletions .rulesync/rules/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Before acting on any non-trivial request - and before delegating - run the `enha

1. **Zod-first contracts.** Every shape is a Zod schema; types are `z.infer`'d, never hand-written. Cross-cutting schemas in `packages/core/src/contracts/schemas/`; each module OWNS its route contract + req/res schemas + `z.infer`'d types in its `contract/` dir - the single source of wire truth, nothing else re-declares a wire shape. `composeContract` (`@openora/core/contracts`) owns only `health`; the composition root (`tools/gen/build-contract.ts` here, the consumer's entry when deployed) composes each enabled module's `/contract` slice into the one runtime contract the SDK links against. ADR-0021/0025.
2. **oRPC + Hono.** oRPC owns route definition + Zod validation + OpenAPI emit; its `OpenAPIHandler` mounts on a Hono server. DI is a functional `Container` (`@openora/core/server`) - typed-token factories, no decorators, no `reflect-metadata`. ADR-0009.
3. **Plugin host.** `definePlugin({ id, dependsOn, register })` is the only way new functionality enters. Everything (core modules included) loads through `extensions.config.ts`.
3. **Plugin host.** Typed plugin objects are the only way new functionality enters. Everything (core modules included) loads through `extensions.config.ts`.
4. **Headless.** Backend modules + contracts + SDK surface only. UI lives in the consumer, which imports `@openora/core/react` (hooks, typed client, auth, realtime). No UI packages here.
5. **Explicit > magic.** No auto-discovery, no decorators. Everything greppable; every wiring point a typed call.
6. **AI-first.** Every module has an `AGENTS.md`; every scaffold a command; contracts queryable via the `oss-dev` MCP server + generated `docs/catalog.json`.
Expand All @@ -42,7 +42,7 @@ packages/
core/ # @openora/core - THE single published package (ADR-0025). Subpaths:
src/contracts/ # isomorphic: composeContract + healthContract, base zod schemas (schemas/), adapter interfaces + DI tokens (adapters/)
src/react/ # domain-agnostic SDK: createClient, typed client, auth, realtime. No UI.
src/server/ # node engine: kernel (logger, EventBus + EVENT_BUS, Container), plugin-host (definePlugin, ModuleRegistry, loader), db (DrizzleService), auth (better-auth + AdminGuard), runtime (createApp - domain-agnostic, single-tenant). Subpaths: /orm, /migrate
src/server/ # node engine: kernel (logger, EventBus + EVENT_BUS, Container), plugin-host (Plugin, ModuleRegistry, loader), db (DrizzleService), auth (better-auth + AdminGuard), runtime (createApp - domain-agnostic, single-tenant). Subpaths: /orm, /migrate
src/compliance/ # sealed-token list + assertSealedServicesBound (engine); also the compliance domain (/contracts, /schema, /plugins)
src/<domain>/ # 9 folded domains (casino, cms, compliance, engagement, pam, wallet, iam, audit, admin-console), exposed as @openora/core/<domain>/{contracts,schema,plugins,server,react}. The BARE root (@openora/core/<domain>) is the public consumer surface: an isomorphic contract barrel (schemas, enum triples, z.infer types; multi-slice domains namespace per slice, eg `import { chat } from '@openora/core/engagement'`) - never server code. Services/routers/plugin live under /server; tables under /schema. A domain imports engine zones + a sibling's read-only /schema only - never a sibling's internals.
src/<domain>/<module>/drizzle/ # each module owns its drizzle.config.ts + migrations/ history (ADR-0027); scripts/generate-all.mjs runs them all
Expand Down
2 changes: 1 addition & 1 deletion .rulesync/subagents/module-author.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Creates the module as a standalone package with all required files and registers
| `service/<name>.service.ts` | Business logic as plain async methods. No HTTP concepts. Inject `DrizzleService` + `EventBus`. |
| `adapters/<vendor>/` | Impls of any adapter ports (port + token in `packages/core/src/contracts/adapters/`). |
| `router/index.ts` | Thin oRPC wiring; admin routes call `await adminGuard.assert(context)` first. |
| `plugin.ts` | `definePlugin` - DI wiring only. |
| `plugin.ts` | `Plugin<CoreTokenCatalog>` object - DI wiring only. |
| `AGENTS.md` | ONLY what code can't say: invariants, rationale, gotchas, extension seams. No route/table/layout listings - they duplicate `contract/`/`schema/` and drift. |

Headless repo: build no UI. After filling in: `pnpm regen` (migration + OpenAPI + catalog), then `pnpm verify` and fix everything.
Expand Down
2 changes: 1 addition & 1 deletion .rulesync/subagents/plugin-author.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ ctx.jobs.worker({ queue, schema, handler, onDeadLetter }); // process JOB_QUEUE
ctx.mcp.tool(definition); // expose a new MCP tool
```

No decorators, no controllers - `definePlugin({ id, dependsOn, register })` wired by the functional Container (ADR-0009). DB tables: a `pgTable` in the plugin's own `schema/index.ts`, then `pnpm regen`.
No decorators, no controllers - `{ id, dependsOn, register } satisfies Plugin<CoreTokenCatalog>` wired by the functional Container (ADR-0009). DB tables: a `pgTable` in the plugin's own `schema/index.ts`, then `pnpm regen`.

## Swapping a vendor adapter

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ generator and fails on an uncommitted diff. So if you touched schemas or routes,
- Anything that touches the database is tested against real Postgres (`createTestDb` from
`@openora/core/testing`), never a faked query builder. Only external vendors and cross-module
ports are doubled.
- New functionality enters only via `definePlugin`. No auto-discovery, no magic.
- New functionality enters only via a plugin object. No auto-discovery, no magic.
- ASCII only in code. Short dashes (-) only.
- Don't hand-edit generated files: drizzle migrations, `docs/openapi.json`, `docs/catalog.json`,
and the rulesync-generated agent files (`AGENTS.md`, `CLAUDE.md`, `.codex/config.toml`,
Expand Down
4 changes: 2 additions & 2 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Before acting on any non-trivial request - and before delegating to an agent - r

1. **Zod-first contracts.** Every shape is a Zod schema; types are `z.infer`'d, never hand-written. Cross-cutting schemas in `packages/core/src/contracts/schemas/`; each module OWNS its route contract + req/res schemas + `z.infer`'d types in its `contract/` dir - the single source of wire truth, nothing else re-declares a wire shape. `composeContract` (`@openora/core/contracts`) owns only `health`; the composition root (`tools/gen/build-contract.ts` here, the consumer's entry when deployed) composes each enabled module's `/contract` slice into the one runtime contract the SDK links against. ADR-0021/0025.
2. **oRPC + Hono.** oRPC owns route definition + Zod validation + OpenAPI emit; its `OpenAPIHandler` mounts on a Hono server. DI is a functional `Container` (`@openora/core/server`) - typed-token factories, no decorators, no `reflect-metadata`. ADR-0009.
3. **Plugin host.** `definePlugin({ id, dependsOn, register })` is the only way new functionality enters. Everything (core modules included) loads through `extensions.config.ts`.
3. **Plugin host.** Typed plugin objects are the only way new functionality enters. Everything (core modules included) loads through `extensions.config.ts`.
4. **Headless.** Backend modules + contracts + SDK surface only. UI lives in the consumer, which imports `@openora/core/react` (hooks, typed client, auth, realtime). No UI packages here.
5. **Explicit > magic.** No auto-discovery, no decorators. Everything greppable; every wiring point a typed call.
6. **AI-friendly.** Every module has an `AGENTS.md`; every scaffold a command; contracts queryable via the `oss-dev` MCP server + generated `docs/catalog.json`.
Expand All @@ -52,7 +52,7 @@ packages/
core/ # @openora/core - THE single published package (ADR-0025). Subpaths:
src/contracts/ # isomorphic: composeContract + healthContract, base zod schemas (schemas/), adapter interfaces + DI tokens (adapters/)
src/react/ # domain-agnostic SDK: createClient, typed client, auth, realtime. No UI.
src/server/ # node engine: kernel (logger, EventBus + EVENT_BUS, Container), plugin-host (definePlugin, ModuleRegistry, loader), db (DrizzleService), auth (better-auth + AdminGuard), runtime (createApp - domain-agnostic, single-tenant). Subpaths: /orm, /migrate
src/server/ # node engine: kernel (logger, EventBus + EVENT_BUS, Container), plugin-host (Plugin, ModuleRegistry, loader), db (DrizzleService), auth (better-auth + AdminGuard), runtime (createApp - domain-agnostic, single-tenant). Subpaths: /orm, /migrate
src/compliance/ # sealed-token list + assertSealedServicesBound (engine); also the compliance domain (/contracts, /schema, /plugins)
src/<domain>/ # 9 folded domains (casino, cms, compliance, engagement, pam, wallet, iam, audit, admin-console), exposed as @openora/core/<domain>/{contracts,schema,plugins,server,react}. The BARE root (@openora/core/<domain>) is the public consumer surface: an isomorphic contract barrel (schemas, enum triples, z.infer types; multi-slice domains namespace per slice, eg `import { chat } from '@openora/core/engagement'`) - never server code. Services/routers/plugin live under /server; tables under /schema. A domain imports engine zones + a sibling's read-only /schema only - never a sibling's internals.
src/<domain>/<module>/drizzle/ # each module owns its drizzle.config.ts + migrations/ history (ADR-0027); scripts/generate-all.mjs runs them all
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The platform ships the backend surface (auth, wallet, player management, complia
## Highlights

- **Headless by design** - backend modules, contracts, and an SDK consumption surface only. No UI ships here; you own the frontend.
- **Plugin host** - `definePlugin({ id, dependsOn, register })` is the single way new functionality enters the system. Overlay a folder or install an npm package; same contract.
- **Plugin host** - typed plugin objects are the single way new functionality enters the system. Overlay a folder or install an npm package; same contract.
- **Zod-first contracts** - every shape is a Zod schema; types are inferred, never hand-written. Routes are oRPC on Hono with OpenAPI emitted at build time.
- **Explicit wiring** - a small functional DI container with typed tokens. No decorators, no auto-discovery; everything is greppable.
- **Swappable vendor seams** - PSP, KYC, aggregator, chat, realtime transport, job queue, and message broker are ports with default in-process drivers and adapter overrides.
Expand Down Expand Up @@ -108,13 +108,13 @@ Generates the module under `packages/core/src/<domain>/<name>/`, wires its domai

### Add an extension (overlay plugin)

Drop a folder under `extensions/<name>/` or point to an npm package. Both use the same `definePlugin` contract:
Drop a folder under `extensions/<name>/` or point to an npm package. Both use the same plugin-object contract:

```typescript
// extensions/my-feature/plugin.ts
import { definePlugin } from '@openora/core/server';
import type { CoreTokenCatalog, Plugin } from '@openora/core/server';

export default definePlugin({
export default {
id: 'my-feature',
dependsOn: ['identity', 'wallet'], // optional load-order hint
register(ctx) {
Expand All @@ -123,7 +123,7 @@ export default definePlugin({
ctx.events.on('wallet.deposit.completed', handler);
ctx.mcp.tool({ name: 'my-tool', description: '...', handler });
},
});
} as const satisfies Plugin<CoreTokenCatalog>;
```

Then register it in `extensions.config.ts`.
Expand Down
6 changes: 3 additions & 3 deletions docs/adapters/error-tracking.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ provide it; loading the overlay last makes its binding win.
```ts
// extensions/sentry/plugin.ts (consumer)
import { ERROR_TRACKING } from '@openora/core/contracts';
import { definePlugin } from '@openora/core/server';
import type { CoreTokenCatalog, Plugin } from '@openora/core/server';
import * as Sentry from '@sentry/node';

export default definePlugin({
export default {
id: 'sentry',
register(ctx) {
const dsn = process.env['SENTRY_DSN'];
Expand All @@ -74,7 +74,7 @@ export default definePlugin({
},
}));
},
});
} as const satisfies Plugin<CoreTokenCatalog>;
```

Register it last in `extensions.config.ts` (a `kind: 'infra'` overlay). A PostHog/Rollbar shop
Expand Down
12 changes: 6 additions & 6 deletions docs/adapters/kyc.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,16 @@ export class SumsubKycAdapter implements KycAdapter {
```ts
// extensions/sumsub-kyc/plugin.ts
import { KYC_ADAPTER } from '@openora/core/contracts';
import { definePlugin } from '@openora/core/server';
import type { CoreTokenCatalog, Plugin } from '@openora/core/server';
import { SumsubKycAdapter } from './src/sumsub-kyc-adapter.js';

export default definePlugin({
export default {
id: 'sumsub-kyc',
dependsOn: ['identity'],
register(ctx) {
ctx.provide(KYC_ADAPTER, () => new SumsubKycAdapter());
},
});
} as const satisfies Plugin<CoreTokenCatalog>;
```

4. Register in `extensions.config.ts` **after** the `identity` entry.
Expand Down Expand Up @@ -182,11 +182,11 @@ export class HostedKycAdapter implements KycAdapter {
```ts
// extensions/hosted-kyc/plugin.ts
import { KYC_ADAPTER, KYC_WEBHOOK_VERIFIER } from '@openora/core/contracts';
import { definePlugin } from '@openora/core/server';
import type { CoreTokenCatalog, Plugin } from '@openora/core/server';
import { HostedKycAdapter } from './src/hosted-kyc-adapter.js';
import { HostedKycWebhookVerifier } from './src/hosted-kyc-webhook-verifier.js';

export default definePlugin({
export default {
id: 'hosted-kyc',
dependsOn: ['identity'],
register(ctx) {
Expand All @@ -196,7 +196,7 @@ export default definePlugin({
() => new HostedKycWebhookVerifier(process.env.HOSTED_KYC_WEBHOOK_SECRET),
);
},
});
} as const satisfies Plugin<CoreTokenCatalog>;
```

4. Register in `extensions.config.ts` **after** the `identity` entry. The consumer's
Expand Down
6 changes: 3 additions & 3 deletions docs/adapters/notification.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ export class MyEmailAdapter implements NotificationDeliveryAdapter {
```ts
// apps/api/src/extensions/email-delivery/plugin.ts
import { NOTIFICATION_DELIVERY_ADAPTER } from '@openora/core/contracts';
import { definePlugin } from '@openora/core/server';
import type { CoreTokenCatalog, Plugin } from '@openora/core/server';
import { MyEmailAdapter } from './src/my-email-adapter.js';

export default definePlugin({
export default {
id: 'email-delivery',
dependsOn: ['notifications'],
register(ctx) {
ctx.provide(NOTIFICATION_DELIVERY_ADAPTER, () => new MyEmailAdapter());
},
});
} as const satisfies Plugin<CoreTokenCatalog>;
```

4. Register in `extensions.config.ts` **after** the `notifications` entry.
Expand Down
6 changes: 3 additions & 3 deletions docs/adapters/payment.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,17 @@ export class CustodyPaymentAdapter implements PaymentAdapter {
```ts
// extensions/custody-payment/plugin.ts
import { PAYMENT_ADAPTER, PAYMENT_WEBHOOK_VERIFIER } from '@openora/core/contracts';
import { definePlugin } from '@openora/core/server';
import type { CoreTokenCatalog, Plugin } from '@openora/core/server';
import { CustodyPaymentAdapter } from './src/custody-payment-adapter.js';

export default definePlugin({
export default {
id: 'custody-payment',
dependsOn: ['wallet'],
register(ctx) {
ctx.provide(PAYMENT_ADAPTER, () => new CustodyPaymentAdapter());
// Omit this line to keep the default HmacPaymentWebhookVerifier (PAYMENT_WEBHOOK_SECRET env var).
},
});
} as const satisfies Plugin<CoreTokenCatalog>;
```

4. Register in `extensions.config.ts` **after** the `wallet` entry.
Expand Down
Loading
Loading