> For the complete documentation index, see [llms.txt](https://host2host.onibonje.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://host2host.onibonje.com/docs/14-extensibility-framework.md).

# Extensibility Framework

## 1. Overview

The H2H platform is designed to be **extended without forking core code**. Banks and integration teams need to:

* Add **custom data fields and tables** for partner-specific or country-specific metadata
* Run **logical scripts and scheduled jobs** (validation, enrichment, reporting, reconciliation prep)
* Deploy on **different databases** (PostgreSQL, Oracle, SQL Server) per bank infrastructure policy
* Plug in **new step types, transform engines, and adapters** via JAR modules
* Subscribe to **events and webhooks** for ecosystem integration

This document defines the **extensibility framework**: extension points, APIs, security boundaries, and recommended patterns.

```mermaid
flowchart TB
  subgraph core [Platform Core - stable]
    Routes[Camel Routes]
    StepReg[Step Registry]
    Config[Config Resolver]
  end

  subgraph extend [Extension Layer]
    CustomTables[Custom Metadata / Tables]
    Scripts[Script Engine]
    Jobs[Job Scheduler]
    SPI[SPI Plugins]
    Hooks[Lifecycle Hooks]
    Events[Event Subscribers]
  end

  subgraph infra [Infrastructure Abstraction]
    Persistence[Persistence SPI]
    Scheduler[Delay Scheduler SPI]
    ScriptRuntime[Script Runtime SPI]
  end

  extend --> core
  core --> infra
```

## 2. Extensibility Layers

Extensions are classified by **risk, deployment model, and who builds them**:

| Layer                       | Who extends                      | Deploy model                    | Examples                                                |
| --------------------------- | -------------------------------- | ------------------------------- | ------------------------------------------------------- |
| **L1 — Configuration**      | Integration analyst (admin UI)   | Publish to DB                   | Mappings, rules, schedules, route templates             |
| **L2 — Metadata & scripts** | Integration developer (admin UI) | Publish to DB + approval        | Custom fields, Groovy/JSONata scripts, SQL-like queries |
| **L3 — Scheduled jobs**     | Integration developer / ops      | Publish to DB + approval        | EOD recon prep, report generation, data purge           |
| **L4 — JAR plugins**        | Platform developer               | Maven artifact + rolling deploy | New step types, adapters, transform engines             |
| **L5 — Infrastructure**     | DevOps / platform team           | Config / Helm                   | Database vendor, Kafka cluster, Vault                   |

**Principle:** Prefer L1 → L2 → L3 before L4. Reserve JAR changes for capabilities that cannot be expressed as data or scripts.

***

## 3. Custom Tables and Metadata Extension

### 3.1 Problem

Partners and countries need fields not in the core schema (e.g. regulatory reference numbers, internal cost centers, custom SLA tiers). Hardcoding columns per partner does not scale.

### 3.2 Extension Model

Three complementary mechanisms:

| Mechanism                    | Use when                                                | Storage                                                              |
| ---------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------- |
| **Custom attributes (EAV)**  | Few optional fields per partner/profile                 | `custom_attribute_def` + `custom_attribute_value`                    |
| **Extension entities**       | Structured repeatable child data (e.g. fee schedules)   | `extension_entity_def` + `extension_entity_row` (JSON schema driven) |
| **Partner extension tables** | Large, query-heavy domain data approved by architecture | Dedicated tables via **controlled migration pack**                   |

### 3.3 Custom Attribute Tables (L2)

**`custom_attribute_def`** — defines the field:

| Column                            | Description                              |
| --------------------------------- | ---------------------------------------- |
| `attr_code`                       | Unique code, e.g. `NG_CBN_REPORTING_REF` |
| `scope_type`                      | PARTNER, PROFILE, COUNTRY, PRODUCT       |
| `data_type`                       | STRING, NUMBER, DATE, BOOLEAN, JSON      |
| `required`                        | Validation flag                          |
| `validation_regex`                | Optional pattern                         |
| `ui_label`                        | Admin portal display                     |
| `effective_from` / `effective_to` | Validity                                 |

**`custom_attribute_value`** — stores values:

| Column                      | Description                    |
| --------------------------- | ------------------------------ |
| `attr_code`                 | FK to definition               |
| `scope_id`                  | partner\_id, profile\_id, etc. |
| `value_text` / `value_json` | Typed storage                  |

**Runtime access:**

```java
CustomAttributeService.get(partnerId, "NG_CBN_REPORTING_REF");
```

Scripts and transforms reference attributes via expression: `${custom.partner.NG_CBN_REPORTING_REF}`.

### 3.4 Extension Entities (structured custom data)

For tabular custom data (multiple rows per partner):

**`extension_entity_def`**

| Column        | Description                    |
| ------------- | ------------------------------ |
| `entity_code` | e.g. `PARTNER_FEE_SCHEDULE`    |
| `json_schema` | JSON Schema for row validation |
| `scope_type`  | PARTNER, COUNTRY               |

**`extension_entity_row`**

| Column        | Description                        |
| ------------- | ---------------------------------- |
| `entity_code` | FK                                 |
| `scope_id`    | partner\_id                        |
| `row_data`    | JSON document conforming to schema |
| `row_order`   | Optional ordering                  |

Admin UI: dynamic form generated from `json_schema`.

### 3.5 Approved Custom Tables (L4/L5 — controlled)

When EAV/JSON is insufficient (heavy reporting, millions of rows):

1. Team submits **extension migration pack** (`V100__ext_partner_ledger.sql`)
2. Architecture review + DBA approval
3. Flyway applies to `ext_` prefixed tables only
4. Access via **Extension Repository SPI** — never direct SQL from scripts

**Naming convention:** `ext_{domain}_{entity}` e.g. `ext_recon_partner_ledger`

**Rule:** Core platform tables (`partner`, `integration_profile`, etc.) are never altered by extensions — only additive `ext_*` tables or EAV.

### 3.6 Custom Tables in Admin Portal

| Screen             | Capability                                             |
| ------------------ | ------------------------------------------------------ |
| Attribute designer | Create/edit `custom_attribute_def`                     |
| Entity designer    | Upload JSON Schema, manage extension entities          |
| Data editor        | CRUD `extension_entity_row` per partner                |
| Migration console  | (Super admin) Apply approved extension migration packs |

***

## 4. Logical Operation Scripts

### 4.1 Script Step Type (`SCRIPT_EXECUTE`)

A first-class route step that runs an admin-defined script against the Camel exchange.

**Table: `script_def`**

| Column            | Description                                       |
| ----------------- | ------------------------------------------------- |
| `script_id`       | Unique ID                                         |
| `name`            | Human-readable name                               |
| `language`        | GROOVY, JSONATA, JAVASCRIPT (GraalVM)             |
| `script_body`     | Source code                                       |
| `input_context`   | EXCHANGE\_BODY, HEADERS, CUSTOM\_ATTRS, CANONICAL |
| `timeout_ms`      | Max execution time                                |
| `sandbox_profile` | RESTRICTED (default), STANDARD                    |

**Registered in `StepRegistry`:**

```
SCRIPT_EXECUTE → scriptExecuteProcessor
```

**Example use cases:**

| Use case              | Script role                                      |
| --------------------- | ------------------------------------------------ |
| Custom validation     | Check business rule not expressible in rule grid |
| Enrichment            | Compute derived field before Finacle post        |
| Routing decision      | Set `h2h.routeAction` based on complex logic     |
| Format pre-processing | Fix partner-specific CSV quirks before JOLT      |

### 4.2 Script Runtime Architecture

```mermaid
flowchart LR
  Step[ScriptExecuteProcessor]
  Sandbox[Script Sandbox]
  Engine[Script Engine SPI]
  Context[ScriptContext]

  Step --> Sandbox
  Sandbox --> Engine
  Engine --> Context
  Context --> Exchange[Camel Exchange]
```

**Module:** `h2h-script-engine`

**`ScriptEngine` SPI:**

```java
public interface ScriptEngine {
    String language();
    ScriptResult execute(ScriptRequest request);
}

public record ScriptRequest(
    String scriptBody,
    ScriptContext context,
    Duration timeout,
    SandboxProfile profile
) {}
```

**`ScriptContext` exposes (whitelist only):**

| Binding      | Access                                           |
| ------------ | ------------------------------------------------ |
| `body`       | Message body (read/write per profile)            |
| `headers`    | Exchange headers                                 |
| `properties` | `h2h.*` exchange properties                      |
| `custom`     | Custom attributes for current partner            |
| `log`        | Structured logger (no PII in debug without mask) |
| `config`     | Read-only resolved profile snapshot              |

**Not exposed:** `System`, file I/O, network, raw JDBC, reflection, classloading.

### 4.3 Script Languages

| Language       | Engine            | Best for                            |
| -------------- | ----------------- | ----------------------------------- |
| **JSONata**    | Built-in (Camel)  | Field expressions, light transforms |
| **Groovy**     | JSR-223 + sandbox | Multi-line business logic           |
| **JavaScript** | GraalVM polyglot  | Teams preferring JS syntax          |

**Default for analysts:** JSONata expressions in validation/routing rules (L1).

**Default for developers:** Groovy in `SCRIPT_EXECUTE` steps (L2).

### 4.4 Script Governance

| Control         | Implementation                                                  |
| --------------- | --------------------------------------------------------------- |
| Draft / publish | Same workflow as other config                                   |
| Test harness    | Run script against sample exchange in sandbox                   |
| Static analysis | Block dangerous imports (`java.io`, `Runtime`, etc.)            |
| Timeout         | Hard kill at `timeout_ms`                                       |
| Audit           | Log script\_id, version, correlation ID (not full body in prod) |
| Approval        | Scripts in PRODUCTION require integration supervisor            |

***

## 5. Scheduled Jobs and Batch Operations

### 5.1 Job Definition (database-driven)

**Table: `job_def`**

| Column             | Description                                                                                                       |
| ------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `job_id`           | Unique ID                                                                                                         |
| `job_code`         | e.g. `EOD_RECON_AGGREGATE`                                                                                        |
| `job_type`         | SCRIPT, SQL\_TASK, CAMEL\_ROUTE, HTTP\_CALLBACK                                                                   |
| `cron_expression`  | Schedule expression (cron notation) — used only to **compute** `next_execute_at`; not executed by in-process cron |
| `schedule_type`    | `CRON`, `INTERVAL`, `ONE_TIME`                                                                                    |
| `interval_seconds` | For `INTERVAL` type (optional)                                                                                    |
| `delay_backbone`   | `KAFKA`, `RABBITMQ` (inherits platform default if null)                                                           |
| `timezone`         | e.g. `Africa/Lagos`                                                                                               |
| `scope`            | GLOBAL, COUNTRY, PARTNER                                                                                          |
| `scope_id`         | Optional filter                                                                                                   |
| `payload`          | JSON — script\_id, route\_id, SQL task ref, etc.                                                                  |
| `enabled`          | Boolean                                                                                                           |
| `max_runtime_ms`   | Timeout                                                                                                           |
| `on_failure`       | ALERT, RETRY, DISABLE                                                                                             |

**Table: `job_execution_log`**

| Column                       | Description              |
| ---------------------------- | ------------------------ |
| `execution_id`               | UUID                     |
| `job_id`                     | FK                       |
| `started_at` / `finished_at` | Timestamps               |
| `status`                     | RUNNING, SUCCESS, FAILED |
| `rows_affected`              | Optional                 |
| `error_message`              | On failure               |

### 5.2 Job Types

| Type               | Description                                       | Module                |
| ------------------ | ------------------------------------------------- | --------------------- |
| **SCRIPT**         | Run `script_def` in batch context (no exchange)   | `h2h-script-engine`   |
| **SQL\_TASK**      | Parameterized, approved query via Persistence SPI | `h2h-persistence-spi` |
| **CAMEL\_ROUTE**   | Trigger `direct:job-{jobCode}` route              | `h2h-job-scheduler`   |
| **HTTP\_CALLBACK** | POST to internal webhook with job payload         | `h2h-job-scheduler`   |

### 5.3 Broker-Delayed Job Scheduler (no in-process cron)

**Module:** `h2h-job-scheduler`

Scheduled operations are **never** driven by Quartz, Spring `@Scheduled`, or Camel timer/cron components. The broker holds the schedule as **delayed, durable messages** so jobs still fire after an outage.

```mermaid
flowchart TB
  subgraph config [Configuration]
    JobDef[job_def in DB]
    State[job_schedule_state]
  end

  subgraph scheduler [h2h-job-scheduler]
    Bootstrap[ScheduleBootstrap]
    Publisher[DelayedJobPublisher]
    Executor[JobExecutor]
  end

  subgraph broker [Delay Backbone]
    RabbitDelay[RabbitMQ x-delayed-message]
    KafkaSchedule[Kafka h2h.jobs.schedule]
    ExecuteQ[h2h.jobs.execute]
  end

  subgraph workers [Execution]
    Script[Script Engine]
    SQL[SqlTaskExecutor]
    Camel[Camel Producer]
  end

  JobDef --> Bootstrap
  Bootstrap --> Publisher
  Publisher --> RabbitDelay
  Publisher --> KafkaSchedule
  RabbitDelay --> ExecuteQ
  KafkaSchedule -->|DelayedJobRelay when executeAt due| ExecuteQ
  ExecuteQ --> Executor
  Executor --> Script
  Executor --> SQL
  Executor --> Camel
  Executor -->|reschedule next| Publisher
  Executor --> State
```

#### 5.3.1 Lifecycle

| Phase          | Behaviour                                                                                                                                                                         |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Bootstrap**  | On startup and `CONFIG_PUBLISHED` for jobs: read enabled `job_def`, compute `next_execute_at` (timezone + holiday calendar), publish delayed message if none pending for `job_id` |
| **Delay**      | Message sits in RabbitMQ delayed exchange or Kafka schedule topic until `executeAt`                                                                                               |
| **Execute**    | Consumer on `h2h.jobs.execute` runs job (SCRIPT / SQL\_TASK / CAMEL\_ROUTE / HTTP\_CALLBACK), writes `job_execution_log`                                                          |
| **Reschedule** | On success: compute next occurrence from `schedule_expression` → publish next delayed message (self-perpetuating chain for recurring jobs)                                        |
| **Retry**      | On failure with `on_failure=RETRY`: publish delayed retry with exponential backoff via same backbone                                                                              |
| **Outage**     | Broker retains message; execution resumes when any runtime instance consumes — **no missed cron tick**                                                                            |

#### 5.3.2 RabbitMQ delay

```
Exchange: h2h.jobs.delayed  (type: x-delayed-message, x-delayed-type: direct)
Queue:    h2h.jobs.execute
Header:   x-delay = milliseconds until delivery
```

Requires RabbitMQ **delayed message plugin**. Messages are persistent (`delivery_mode=2`).

#### 5.3.3 Kafka delay

Kafka has no native per-message delay. The platform uses:

1. Publish `JobScheduleCommand` to `h2h.jobs.schedule` with `executeAt` (ISO-8601) in headers/body.
2. `DelayedJobRelay` (in `h2h-job-scheduler`) consumes schedule commands and, when `executeAt <= now`, forwards to `h2h.jobs.execute`.
3. For long delays, relay re-publishes to `h2h.jobs.schedule` with updated delay bucket (tiered delay pattern) — or **prefer RabbitMQ** when sub-minute precision and long outages matter.

**Idempotency:** `job_id` + `schedule_version` + `planned_execute_at` in message headers; executor skips if `job_execution_log` already shows SUCCESS for that planned slot.

#### 5.3.4 job\_schedule\_state table

| Column                | Description                           |
| --------------------- | ------------------------------------- |
| `job_id`              | FK to `job_def`                       |
| `schedule_version`    | Bumped on each publish of `job_def`   |
| `next_execute_at`     | Computed next fire time               |
| `pending_message_ref` | Broker message id / offset (optional) |
| `last_executed_at`    | Last successful run                   |

#### 5.3.5 Design rules

* Job definitions loaded from DB — not hardcoded timers
* On `CONFIG_PUBLISHED` for jobs → bump `schedule_version`, bootstrap republishes delayed message (supersedes stale pending)
* **No Quartz**, **no JDBC job store**, **no pod-local cron**
* Competing consumers on `h2h.jobs.execute` for horizontal scale; executor must be idempotent

### 5.4 SQL Task (controlled logical operations)

Admins do **not** run arbitrary SQL. They reference **approved SQL tasks**:

**Table: `sql_task_def`**

| Column              | Description                     |
| ------------------- | ------------------------------- |
| `task_code`         | e.g. `AGGREGATE_DAILY_PAYMENTS` |
| `description`       | Purpose                         |
| `parameter_schema`  | JSON Schema for bind parameters |
| `dialect_overrides` | Optional per-DB SQL variants    |
| `approved_by`       | DBA / architect sign-off        |

**SQL is stored hashed and versioned** — created by platform team, **selected** by ops in job definition.

**Execution:** `SqlTaskExecutor` uses Persistence SPI with named parameters only — no string concatenation.

### 5.5 Example Jobs

| Job                      | Type                  | Purpose                                      |
| ------------------------ | --------------------- | -------------------------------------------- |
| `EOD_RECON_AGGREGATE`    | SQL\_TASK             | Aggregate daily payments for recon workbench |
| `PURGE_IDEMPOTENCY_KEYS` | SQL\_TASK             | Delete expired idempotency records           |
| `PARTNER_STATEMENT_POLL` | CAMEL\_ROUTE          | Trigger statement generation route           |
| `CUSTOM_FEE_CALC`        | SCRIPT                | Nightly fee calculation into `ext_*` table   |
| `REGULATORY_EXPORT_NG`   | CAMEL\_ROUTE + SCRIPT | Country-specific regulatory file build       |

***

## 6. Database-Agnostic Persistence

> **Canonical reference:** [45 Database-Agnostic Persistence](/docs/45-database-agnostic-persistence.md)

### 6.1 Goals

| Goal                           | Approach                                          |
| ------------------------------ | ------------------------------------------------- |
| Support multiple RDBMS         | PostgreSQL (primary), Oracle, SQL Server          |
| No vendor SQL in business code | Repository pattern + SPI                          |
| Portable migrations            | Flyway with dialect-specific scripts where needed |
| Testability                    | Testcontainers per dialect in CI                  |

### 6.2 Persistence SPI Architecture

**Module:** `h2h-persistence-spi`

```mermaid
flowchart TB
  subgraph app [Application Layer]
    ConfigRepo[ConfigRepository]
    JobRepo[JobRepository]
    ExtRepo[ExtensionRepository]
  end

  subgraph spi [Persistence SPI]
    DSL[QueryDSL / jOOQ abstraction]
    JPA[JPA repositories - config domain]
  end

  subgraph drivers [Database Drivers]
    PG[(PostgreSQL)]
    ORA[(Oracle)]
    MSSQL[(SQL Server)]
  end

  app --> spi
  spi --> drivers
```

**Layers:**

| Layer                         | Technology                            | Responsibility                                           |
| ----------------------------- | ------------------------------------- | -------------------------------------------------------- |
| Config domain                 | JPA/Hibernate                         | CRUD on platform config entities — dialect via Hibernate |
| Extension / reporting queries | jOOQ or Spring Data JDBC              | Type-safe queries, dialect abstraction                   |
| SQL tasks                     | `SqlTaskExecutor` + dialect overrides | Approved batch SQL                                       |
| Migrations                    | Flyway                                | `db/migration/common/` + `db/migration/{dialect}/`       |

### 6.3 Configuration

```yaml
h2h:
  persistence:
    vendor: postgresql   # postgresql | oracle | sqlserver
    schema: h2h_config
```

**Hibernate:**

```yaml
spring:
  jpa:
    database-platform: ${H2H_DB_PLATFORM:org.hibernate.dialect.PostgreSQLDialect}
```

**Flyway layout:**

```
db/migration/
├── common/           # Vendor-neutral DDL (standard SQL types)
│   V1__partner.sql
├── postgresql/       # PG-specific indexes, JSONB
│   V1.1__partner_pg.sql
├── oracle/
│   V1.1__partner_ora.sql
└── sqlserver/
    V1.1__partner_mssql.sql
```

### 6.4 Portability Rules

| Rule                                                 | Rationale                                               |
| ---------------------------------------------------- | ------------------------------------------------------- |
| Use `VARCHAR`, `NUMERIC`, `TIMESTAMP WITH TIME ZONE` | ANSI-aligned types                                      |
| Avoid DB-specific JSON operators in core queries     | Use JPA `@JdbcTypeCode(SqlTypes.JSON)` or abstraction   |
| Limit raw SQL to `sql_task_def` and jOOQ layer       | Single place for dialect handling                       |
| No `LIMIT` / `ROWNUM` / `TOP` in shared scripts      | Use SPI pagination API                                  |
| Sequences vs identity                                | Abstract via JPA `@GeneratedValue` strategy per profile |

### 6.5 JSON Column Strategy

Custom attributes and extension rows use JSON columns.

| Database   | Type                           | Access                   |
| ---------- | ------------------------------ | ------------------------ |
| PostgreSQL | `JSONB`                        | Hibernate JSON mapping   |
| Oracle     | `JSON` (12c+) or `CLOB`        | Dialect-specific mapping |
| SQL Server | `NVARCHAR(MAX)` + `JSON_CHECK` | Dialect-specific mapping |

**Abstraction:** `JsonColumnMapper` SPI hides vendor differences from application code.

***

## 7. JAR Plugin Extensibility (SPI)

### 7.1 Service Provider Interface Model

**Module:** `h2h-extension-api`

| SPI                            | Purpose                                                     | Discovery                              |
| ------------------------------ | ----------------------------------------------------------- | -------------------------------------- |
| `StepTypeProvider`             | Register new route step codes                               | `META-INF/services/` or Spring beans   |
| `TransformEngineProvider`      | New transform engine                                        | SPI + admin engine registry            |
| `ScriptEngine`                 | New script language                                         | SPI                                    |
| `JobExecutorProvider`          | New job types                                               | SPI                                    |
| `CoreBankingOperationProvider` | New CBS operations (Finacle via `FinacleOperationProvider`) | `h2h-core-banking-api` SPI             |
| `FinacleOperationProvider`     | Finacle-specific operation extensions                       | Extends `CoreBankingOperationProvider` |
| `ReconMatcherProvider`         | Custom reconciliation matchers                              | SPI                                    |
| `HookProvider`                 | Lifecycle hooks (see §8)                                    | SPI                                    |

**Example — custom step:**

```java
@Component
public class BlockchainVerifyStep implements StepTypeProvider {
    @Override public String stepCode() { return "BLOCKCHAIN_VERIFY"; }
    @Override public Processor processor() { return blockchainVerifyProcessor; }
}
```

Auto-registered in `StepRegistry` at startup. Admin can add `BLOCKCHAIN_VERIFY` to route templates once JAR is deployed.

### 7.2 Plugin JAR Packaging

```
h2h-plugin-{name}/
├── pom.xml
├── src/main/java/.../MyStepProvider.java
└── META-INF/spring/...AutoConfiguration.imports
```

Runtime enables via:

```yaml
h2h:
  plugins:
  - h2h-plugin-blockchain-verify
```

***

## 8. Lifecycle Hooks

Hooks allow logic **before/after** standard steps without modifying core routes.

**Table: `hook_def`**

| Column       | Description                                                         |
| ------------ | ------------------------------------------------------------------- |
| `hook_point` | PRE\_INGEST, POST\_TRANSFORM, PRE\_FINACLE, POST\_FINACLE, PRE\_ACK |
| `script_id`  | FK to `script_def`                                                  |
| `partner_id` | Optional scope                                                      |
| `priority`   | Execution order                                                     |

**Execution:** `HookExecutor` runs matching hooks at each point in the pipeline.

**Difference from route steps:** Hooks are **cross-cutting** — apply across templates without editing the template itself.

***

## 9. Event and Webhook Extensibility

### 9.1 Platform Events (database-driven)

Events use **logical `event_code`** values — physical topics/queues and partition keys are defined in `event_def` and `event_channel_def`. See [46 Database-Driven Events](/docs/46-database-driven-events.md).

| `event_code`       | Extensibility                            |
| ------------------ | ---------------------------------------- |
| `PAYMENT_POSTED`   | Trigger downstream ERP posting           |
| `PAYMENT_FAILED`   | Alerting, case management                |
| `CONFIG_PUBLISHED` | Cache invalidation, external config sync |
| `JOB_COMPLETED`    | Downstream batch chains                  |

### 9.2 Webhook Subscriptions (database-driven)

**Table: `webhook_subscription`**

| Column              | Description                                                |
| ------------------- | ---------------------------------------------------------- |
| `event_code`        | `PAYMENT_POSTED`, `FILE_RECEIVED`, etc. (FK → `event_def`) |
| `target_url`        | HTTPS endpoint                                             |
| `auth_ref`          | Vault credential                                           |
| `filter_expression` | JSONata filter on event payload                            |
| `retry_policy`      | JSON                                                       |

**Module:** `h2h-webhook-dispatcher` — consumes events, delivers webhooks with retry.

### 9.3 Notification subscriptions (email, SMS, push)

Partner **machine** integration uses webhooks; **human** alerts and partner contact SMS/email use the notification layer. See [48 Utility & Notification Integrations](/docs/48-utility-notification-integrations.md).

**Table: `notification_subscription`** — binds `event_code` to `channel_type` (EMAIL, SMS, …), template, provider, and recipient resolver.

**Module:** `h2h-notification-dispatcher` + provider JARs (`h2h-notification-twilio`, `h2h-notification-aws-ses`, …).

***

## 10. Transform and Rules Extensibility

| Extension                  | Layer | Mechanism                                     |
| -------------------------- | ----- | --------------------------------------------- |
| New mapping engine         | L4    | `TransformEngineProvider` SPI                 |
| Custom validation function | L2    | Script in validation rule `customFunction`    |
| Drools decision tables     | L2/L3 | Upload decision table, reference in `ruleset` |
| OPA policies               | L3    | Policy bundle URL in `routing_ruleset`        |

***

## 11. Admin API for Extensions

| Endpoint                                   | Purpose                                                  |
| ------------------------------------------ | -------------------------------------------------------- |
| `POST /api/v1/custom-attributes`           | Define custom fields                                     |
| `POST /api/v1/extension-entities`          | Define extension entity schemas                          |
| `POST /api/v1/scripts`                     | CRUD scripts                                             |
| `POST /api/v1/scripts/{id}/test`           | Sandbox execution                                        |
| `POST /api/v1/jobs`                        | CRUD job definitions                                     |
| `POST /api/v1/jobs/{id}/run`               | Manual trigger                                           |
| `GET /api/v1/jobs/executions`              | Execution history                                        |
| `POST /api/v1/hooks`                       | CRUD lifecycle hooks                                     |
| `POST /api/v1/webhooks`                    | CRUD webhook subscriptions                               |
| `POST /api/v1/notifications/subscriptions` | CRUD email/SMS subscriptions                             |
| `POST /api/v1/notifications/providers`     | CRUD notification providers                              |
| `GET /api/v1/spi/step-types`               | List registered step codes (from StepRegistry + plugins) |

***

## 12. Security Boundaries for Extensions

| Extension type    | Risk     | Controls                                                            |
| ----------------- | -------- | ------------------------------------------------------------------- |
| Custom attributes | Low      | Schema validation, RBAC                                             |
| Scripts           | High     | Sandbox, timeout, no I/O, approval workflow                         |
| SQL tasks         | Critical | Pre-approved SQL only, DBA sign-off, read-only replicas for reports |
| Custom tables     | Medium   | `ext_` prefix, migration review, no core DDL                        |
| Webhooks          | Medium   | URL allowlist, mTLS, secret in Vault                                |
| JAR plugins       | High     | Code review, security scan, signed artifacts                        |

**Segregation:** Production scripts and jobs require **integration supervisor** approval. SQL tasks require **DBA** approval.

***

## 13. Recommended Module Additions

| Module                   | Responsibility                                              |
| ------------------------ | ----------------------------------------------------------- |
| `h2h-extension-api`      | SPI interfaces, extension DTOs                              |
| `h2h-script-engine`      | Groovy/JS/JSONata execution, sandbox                        |
| `h2h-job-scheduler`      | Broker-delayed scheduling (Kafka / RabbitMQ), job execution |
| `h2h-persistence-spi`    | Database vendor abstraction, SqlTaskExecutor                |
| `h2h-webhook-dispatcher` | Event-to-webhook delivery                                   |
| `h2h-plugin-*`           | Optional capability plugins                                 |

See [Repository Structure](/docs/11-repository-structure.md) for updated layout.

***

## 14. Extensibility Decision Guide

```mermaid
flowchart TD
  Q1{Can rule grid / mapping editor do it?}
  Q1 -->|Yes| L1[L1 Config only]
  Q1 -->|No| Q2{Need custom logic on exchange?}
  Q2 -->|Yes| Q3{Complex multi-line logic?}
  Q3 -->|No| JSONata[JSONata in rules]
  Q3 -->|Yes| Script[L2 Script step or hook]
  Q2 -->|No| Q4{Scheduled / batch?}
  Q4 -->|Yes| Q5{Approved SQL exists?}
  Q5 -->|Yes| JobSQL[L3 SQL job task]
  Q5 -->|No| JobScript[L3 Script or Camel job]
  Q4 -->|No| Q6{New protocol or system?}
  Q6 -->|Yes| JAR[L4 JAR plugin]
  Q6 -->|No| L1
```

| Need                         | Use                              |
| ---------------------------- | -------------------------------- |
| Extra partner fields         | Custom attributes (EAV)          |
| Partner-specific fee table   | Extension entity (JSON schema)   |
| Complex validation           | Script step or JSONata rule      |
| Nightly aggregation          | SQL task job (approved)          |
| New bank system adapter      | JAR plugin (Adapter pattern)     |
| Notify external system       | Webhook subscription             |
| Oracle instead of PostgreSQL | Persistence SPI + Flyway dialect |

***

## 15. Anti-Patterns

| Anti-Pattern                         | Correct approach             |
| ------------------------------------ | ---------------------------- |
| Arbitrary SQL in admin UI            | Approved `sql_task_def` only |
| Scripts with file/network access     | Sandboxed `ScriptEngine`     |
| Altering core tables for one partner | EAV or `ext_*` tables        |
| Vendor SQL in route JARs             | Persistence SPI              |
| One-off Java route per partner       | Script, hook, or config      |

***

## 16. Related Documents

* [Universal Library Extensibility](/docs/20-universal-library-extensibility.md) — cross-solution customization model
* [Database-Driven Configuration](/docs/04-database-driven-configuration.md)
* [Low-Code Admin Platform](/docs/05-low-code-admin-platform.md)
* [Modular JAR Architecture](/docs/03-modular-jar-architecture.md)
* [Design Patterns](/docs/13-design-patterns.md)
* [Security and Compliance](/docs/09-security-and-compliance.md)
* [Repository Structure](/docs/11-repository-structure.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://host2host.onibonje.com/docs/14-extensibility-framework.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
