Skip to main content

Contributing an Extension to the Registry

info

This applies to iDempiere 14 and later. It documents the new way of distributing plug-ins through the official Extension Registry, alongside the existing manual plug-in distribution described in Distributing and Installing Plug-ins in iDempiere. See iDempiere Extension Management for the end-user overview of the feature.

This guide describes how to publish a plug-in so that any iDempiere 14+ instance can find and install it from the Extension Management form.

The short version: you do not upload a jar anywhere. You publish your source, and you send a pull request adding two small text files to a public catalog repository. The iDempiere project builds the binary from your source itself.

Read Acceptance policy first. It decides whether your extension is eligible at all.

The registry repository​

The official registry lives at github.com/idempiere/idempiere-extension-repository. This URL is the default value of IDEMPIERE_EXTENSION_REPOSITORY, baked into idempiere-server.sh, idempiere-server.bat, org.adempiere.server-feature/server.product, org.idempiere.p2/server.product, and both org.idempiere.p2.profile products. Unless an administrator overrides it, every iDempiere installation points here.

Layout:

index.json ← GENERATED, never edit by hand
index-schema.json
metadata-schema.json
extension-spec.md
README.md
extensions/
└── <extension.id>/
├── info.md ← one per extension
├── CHANGELOG.md ← one per extension (optional but expected)
├── assets/ ← screenshots, sample CSV, etc.
└── <version>/
└── metadata.json ← one per released version
scripts/generate_index.py
.github/workflows/
├── validate-extensions.yml
└── generate-index.yml
skills/idempiere-review-extension/

Your contribution to this repository is a new folder under extensions/, a few text files, no binaries. The jar itself is built by the project from your source repository. See The project builds the jar.

Acceptance policy​

Five rules govern what the registry accepts. They are project decisions, not properties of the code (the extension manager will happily install anything it is pointed at). They exist because a catalog that installs code into production ERP systems is a supply-chain surface, and these are the mitigations the project chose.

Open source only​

Every listed extension must be open source, with a public source repository and a clear licence. Closed-source and source-available-but-not-open extensions are not listed.

This is also the technical precondition for the next rule: the project cannot build what it cannot read.

The project builds the jar​

You do not supply a binary. The iDempiere project builds the jar from your published source on project infrastructure, and bundles[].downloadUrl points at that build output.

The reason is narrow and worth stating plainly: a contributor-supplied jar cannot be shown to correspond to the source that was reviewed. Reviewing source and then shipping someone else's binary would make the review pointless. Building from source closes that gap. What users install is what the reviewers read.

Maven is a hard requirement

Your project must build with Maven (Tycho), unattended, from a clean checkout. This is the only build the project's infrastructure can run, so a plug-in with no Maven build (an Eclipse PDE-only project, for example) cannot be accepted until one exists. See Building iDempiere Plug-ins with Maven for how to set one up.

Beyond the build tool itself, the build must succeed with no local paths, no credentialed dependencies, no manual steps, and no artifacts pulled from a repository the project cannot reach.

Automated analysis first​

Submissions go through automated analysis (currently the skills/idempiere-review-extension checklist) before a maintainer reviews them. It reads the source, the metadata, and the 2Pack content, and surfaces anything that needs a human decision: raw SQL in the dictionary content, unexpected META-INF contents, entity-type misuse, metadata that does not match the jar.

This is risk reduction, not a security guarantee. It narrows what a reviewer has to find by hand. It does not certify the extension. See What review looks like.

Must install on vanilla iDempiere​

Your extension must install and work on an unmodified official iDempiere release. Concretely:

  • No patched or forked core.
  • No manual pre-installation steps beyond what the Extension Management form performs.
  • No dependency on another extension unless it is declared in dependencies[] and is itself in the registry.
  • No expectation of tenant data, roles, or configuration that a fresh install does not have.

This is the single most common reason a submission is sent back, and it is easy to check yourself. See Testing an Extension Locally, which walks through testing against a vanilla instance.

Curated start, and listing is not endorsement​

The registry opens with a curated set of established contributors ("top makers") while the process is proven. That gate is deliberately temporary: the intent is to widen access as review capacity and automation mature.

Because of that, the registry is built so that listing never implies endorsement. Presence in the catalog means an extension met the mechanical criteria above and passed review at the time it was merged. It is not a certification, a warranty, an audit, or a statement that the iDempiere project vouches for the extension or its maintainer. Extensions carry their own authorship, licence, and support channel in info.md, and users are expected to evaluate them on that basis.

Designing for no-endorsement from the start is what makes lifting the curated gate a policy change rather than a redesign. Nothing in the catalog, the UI, or the metadata will need to stop making a promise it was never making.

Open at the time of writing

The mechanics behind these decisions, which build infrastructure produces the jars, the accepted licence list, and how the initial contributor set is determined, are still being settled. Check the registry's own README.md for the current operational detail before submitting.

Contribution flow​

  1. Confirm eligibility against the acceptance policy above: open source, buildable with Maven, installs on vanilla.
  2. Fork idempiere/idempiere-extension-repository.
  3. Create extensions/<your.bundle.symbolic.name>/.
  4. Add info.md, optionally CHANGELOG.md and assets/, and <version>/metadata.json, including a link to the public source repository and the commit or tag to build.
  5. Open a pull request.
  6. validate-extensions.yml runs on the PR and checks every metadata.json it can find.
  7. Automated analysis runs, then a maintainer reviews. See What review looks like.
  8. The project builds the jar from your source and publishes it; downloadUrl is set to that artifact. See How the jar is built and hosted.
  9. On merge, generate-index.yml runs scripts/generate_index.py, regenerates index.json, and commits it as github-actions[bot].
Do not touch index.json

Do not add or edit index.json in your PR. It is owned by the workflow. Editing it creates a merge conflict and will be reverted on the next merge to main.

The metadata.json fields​

One file per released version, at extensions/<id>/<version>/metadata.json.

{
"id": "com.example.plugin.name",
"version": "1.0.0",
"idempiereVersion": "14",
"releaseDate": "2026-09-08",
"name": "Human Readable Name",
"description": "One sentence describing this specific version.",
"categories": ["Integration", "Reporting"],
"tags": ["pdf", "watermark"],
"entityType": "XPLG",
"bundles": [
{
"symbolicName": "com.example.plugin.core",
"downloadUrl": "https://<project-build-host>/.../com.example.plugin.core_1.0.0.jar",
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
},
{
"symbolicName": "com.example.plugin.ui",
"downloadUrl": "https://<project-build-host>/.../com.example.plugin.ui_1.0.0.jar",
"sha256": "..."
}
],
"dependencies": [
{ "id": "com.trekglobal.idempiere.rest.api", "version": "1.0.0" }
],
"database": [
{ "id": "postgresql", "version": "17", "extensions": ["vector"] }
]
}

downloadUrl and sha256 describe the project-built artifact (see the rules above and How the jar is built and hosted). You supply the source repository and the commit or tag; the maintainers fill these in against the build they produce. Do not point them at a binary you built yourself.

FieldRequiredRuleEnforced by
idYesUnique identity, conventionally the main bundle's symbolic name. This is what dependencies[].id refers to, and the key of the AD_Extension row.Validator and CI
versionYesOSGi version: ^[0-9]+\.[0-9]+\.[0-9]+(\.[a-zA-Z0-9_-]+)?$, for example 1.0.0 or 1.0.0.20260908. Three segments minimum; 1.0 is rejected.ExtensionMetadataValidator
idempiereVersionYesAn OSGi version range tested against Adempiere.getVersion(). "14" means "14 and above"; use "[14,15)" to pin to a single major.validateIDempiereVersion
nameYesShown in the extension list.Validator
bundlesYesNon-empty array.Validator and CI
bundles[].symbolicNameYesMust match the jar's Bundle-SymbolicName exactly. This is how the manager finds the bundle later for update, enable, disable, and uninstall.Validator and CI
bundles[].downloadUrlSee note belowMust parse as a URI. Points at the project-built artifact, not one you host.CI requires it; the Java validator does not
bundles[].sha256NoHex SHA-256 of the project-built jar. Verified after download; a mismatch aborts the install.installExtension
releaseDateNoStrict yyyy-MM-dd, non-lenient. 2026-13-01 is rejected.Validator
descriptionNoOne line, shown under the name in the list.Validator
categoriesNoString array, unique items. Filterable with category: in the form.Validator
tagsNoString array, unique items. Filterable with tag:.Validator
entityTypeNoThe AD_EntityType your dictionary content belongs to.-
dependencies[]No{id, version}; version is an OSGi range matched against the installed extension's version. The dependency must be present and in state Installed.validateDependencies
database[]Noid must be postgresql or oracle. If present, the list is exhaustive: a database not named is treated as unsupported. version is a minimum. extensions[] is checked against pg_available_extensions (PostgreSQL only).Validator and validateDatabaseRequirement

The downloadUrl asymmetry​

validate-extensions.yml fails a PR when any bundle lacks symbolicName or downloadUrl:

for idx, bundle in enumerate(data['bundles']):
if 'symbolicName' not in bundle or 'downloadUrl' not in bundle:
errors.append(f"Bundle at index {idx} in {file_path} missing 'symbolicName' or 'downloadUrl'")

The runtime validator (ExtensionMetadataValidator) only requires symbolicName. This is why the bundled core extensions in the iDempiere source tree (org.idempiere.acct/metadata.json, org.adempiere.replication/metadata.json, org.adempiere.payment.processor/metadata.json) legitimately omit downloadUrl. They ship with the product and are never downloaded.

note

Those bundled-extension files are not in the registry and are not a template for downloadUrl. Anything published to the registry must carry one.

If a bundle has no downloadUrl at install time, the process aborts with BundleNoDownloadURL.

Because the value now names a project-built artifact, a first submission may legitimately open with the field absent or a placeholder and have it filled in during review. The CI check will fail until it is set. Agree the sequence with the maintainers rather than inventing a URL to get CI green.

Database requirements are exhaustive​

If you declare database, you must list every database you support. validateDatabaseRequirement looks for an entry matching the running database and, finding none, returns DatabaseTypeNotSupported. Omit the whole key if you are database-agnostic. Note that the extensions[] availability check is implemented for PostgreSQL only; on Oracle only the version is compared.

The info.md file​

Rendered as the Details tab in the Extension Management form. Reviewers check for six sections:

# Extension Name

**Summary:** One or two sentences, what it does and why someone would install it.

## 🚀 Features
* Bullets, concrete, user-visible.

## ⚙️ Compatibility
* **iDempiere Version:** 14
* **Java Version:** 17+
* **Database:** PostgreSQL, Oracle

## 📦 Database Changes
* **System PackIn:** ships a System-tenant 2Pack creating ...
* **Tenant PackIn:** download from {url} and install into the destination tenant
* **Tenant CSV Import:** download from {url} and import

## 🛠 Usage & Configuration
Where the new menu/window/field lives, what must be configured before it works.

## 👤 Author / Support
* **Developer:** Name / Organisation
* **Source Code:** https://... (public repository, required)
* **Licence:** GPL-2.0-or-later (required)
* **Issue Tracker:** https://...

Source Code and Licence are not optional. The registry admits open-source extensions only, and the jar is built from that repository, so the link is what the maintainers build from, and the licence is what makes doing so permissible. Name the licence with its SPDX identifier rather than prose.

This section is also where the no-endorsement policy lands in practice. The registry does not vouch for your extension; Author / Support is where a user finds out who does. Give a support channel a stranger can actually use.

Working examples in the iDempiere source tree, written for exactly this purpose:

  • org.idempiere.acct/info.md
  • org.adempiere.replication/info.md
  • org.adempiere.payment.processor/info.md

The Markdown is rendered then sanitised through AEnv.sanitize() (an OWASP HTML sanitiser: blocks, formatting, images, links, styles, tables). Scripts and unlisted tags are removed, so write plain Markdown. Relative image links resolve against the directory of the info.md itself, so ![screenshot](assets/config.png) works if the file is committed under assets/.

CHANGELOG.md is optional but expected; follow keepachangelog. Both files live at the extension level, not inside a version folder. They describe the whole extension across versions.

How the jar is built and hosted​

The project builds the jar from your source. You do not host a binary and you do not choose the downloadUrl. What you provide is a public source repository and the exact commit or tag to build; the resulting artifact is published on project infrastructure and that URL goes into bundles[].downloadUrl.

What this asks of you:

  • The build must be reproducible unattended. Standard Tycho/Maven build, no local paths, no credentialed or private dependencies, no manual steps, no artifacts from repositories the project cannot reach.
  • Tag your releases. A moving branch is not a build input. Point at an immutable commit or tag so that the artifact users install corresponds to a fixed, reviewable state of the source.
  • Keep Bundle-Version in step with the metadata version. The review cross-checks them.
  • The sha256 is computed from the produced artifact, so you generally will not be the one filling it in. If you submit one, it must match; a mismatch aborts installation at the client with BundleSHA256HashNotMatch.

The registry itself still stores no binaries, only the JSON pointing at them.

What the client requires of the URL​

Whatever the project publishes to, the URL must serve the exact bytes whose sha256 is recorded (never re-uploaded under the same URL), be publicly reachable without authentication (the client sends no credentials), and may redirect freely (HttpClient.Redirect.ALWAYS, 30-second timeout). GitHub blob URLs also resolve, because toRawGithubPath() rewrites them to raw.githubusercontent.com automatically.

A note on what is in the registry today

The existing REST API entry points at https://jenkins.idempiere.org/job/idempiere-rest/ws/.../target/...-SNAPSHOT.jar, a project Jenkins build, so already project-built rather than contributor-supplied, but a workspace (/ws/) path to a -SNAPSHOT artifact. A workspace path is mutable and can vanish between builds. Expect entries to move to stable archived-artifact URLs as the build-from-source rule is fully implemented; treat the current one as a transitional state, not the pattern to imitate.

Publish or verify the sha256 wherever you can. It is the only integrity check the client performs (there is no signature verification), and it is what ties the reviewed source, the produced artifact, and the installed bytes together.

Shipping dictionary changes​

Application-dictionary content travels inside your jar, not in the registry:

  • Put the 2Pack at META-INF/2Pack_<version>.zip. The manager detects it with bundle.findEntries("META-INF", "2Pack_*.zip", false) after starting the bundle, and Incremental2PackActivator imports it in the background. The user sees broadcast notifications when the import starts and finishes. See the 2Pack documentation for the format itself.
  • Register your own AD_EntityType and use it for every record.
Never use entity type D

D is reserved for the iDempiere dictionary. Using it makes your records indistinguishable from core and unmanageable by the registry.

  • Successful pack-in details are copied into AD_ExtensionEntity, which is what later lets a user disable your extension (each recorded record is flipped to IsActive='N') and re-enable it. Records your plug-in creates outside a 2Pack are not tracked and will not be deactivated.
  • Uninstall does not roll the 2Pack back. Design your dictionary content so that deactivation is a safe state.

Content that must land in a tenant rather than the System tenant cannot be pushed by the installer. Publish it as a downloadable 2Pack or CSV and document it in the Database Changes section of info.md, as the template above shows.

Publishing a new version​

  • Add a new folder: extensions/<id>/2.0.0/metadata.json.
warning

Never modify a published version's metadata. Instances that already installed it hold a copy of that JSON in AD_Extension.ExtensionMetadata.

  • Update CHANGELOG.md.
  • Update info.md if compatibility or configuration changed.
  • Bump the jar's Bundle-Version to match, and publish it at a new immutable URL.

The indexer collects every version folder, sorts them newest-first, and emits:

"versions": [
{ "version": "2.0.0", "metadataUrl": "https://github.com/idempiere/idempiere-extension-repository/blob/main/extensions/<id>/2.0.0/metadata.json" },
{ "version": "1.0.0", "metadataUrl": ".../1.0.0/metadata.json" }
]

With two or more versions, the form shows a version dropdown, so older releases stay installable. Users on an older version see the Install button change to Update; downgrades are refused with ExtensionBundleNewerVersionInstalled.

What review looks like​

Review is deliberately layered: cheap mechanical checks first, automated analysis second, human judgement last and only on what survives.

  1. CI, validate-extensions.yml on the pull request. Checks required keys, non-empty bundles, symbolicName, and downloadUrl per bundle. Fails fast, and tells you exactly which file and key.
  2. Automated analysis, the skills/idempiere-review-extension checklist, run over the source and the built artifact:
    • Documentation: the six info.md sections present, grammar, semantic accuracy, links resolve.
    • Schema: metadata.json against metadata-schema.json; mandatory fields, version pattern, valid URIs.
    • Bundles: the jar is actually downloaded; version and hash cross-checked against the metadata; META-INF inspected; 2Pack naming (2Pack_<version>) verified; entity type and System-tenant creation confirmed.
    • SQL: any raw SQL statement inside the 2Pack XML is extracted into ExtensionSQLStatements.sql, annotated with source file and line, for a human to read.
  3. Human review, a maintainer decides on what the previous layers surfaced, plus the judgement calls no tool makes: is this genuinely open source, does the build come out clean, does it install on vanilla, and does it follow iDempiere practice (proper OSGi structure, no hardcoded values, own entity type, clear licence).

The automated layer covers what a reviewer cannot sustain by hand across every submission: reading all the dictionary content, opening the jar, comparing it against the metadata. It raises findings for a person to judge. It does not approve anything on its own, and it is not a security guarantee.

Expect your source and the resulting jar to be read. Checking these points yourself first is the fastest way through.

Pre-PR checklist​

Eligibility

  • Source repository is public and the licence is stated with its SPDX identifier.
  • The project builds with Maven (Tycho), unattended, from a clean checkout, no local paths, private dependencies, or manual steps.
  • An immutable commit or tag is nominated for the build.
  • Installs and works on vanilla iDempiere: no core patches, no undeclared prerequisites, no manual pre-steps.
  • Every prerequisite extension is declared in dependencies[] and is itself in the registry.

Metadata

  • Jar's Bundle-SymbolicName matches bundles[].symbolicName, and Bundle-Version matches the metadata version.
  • metadata.json has id, name, version, idempiereVersion, bundles.
  • downloadUrl and sha256 agreed with the maintainers against the project-built artifact, not a binary you host.
  • version has three numeric segments; releaseDate is yyyy-MM-dd.
  • idempiereVersion range actually matches the version you tested against.
  • database[] lists all supported databases, or is omitted entirely.
  • Own AD_EntityType; 2Pack named META-INF/2Pack_<version>.zip.

Documentation

  • info.md has all six sections, including Source Code and Licence; every link resolves; images committed under assets/.
  • CHANGELOG.md updated.
  • index.json not touched.

Verification

  • Installed and exercised on a vanilla instance first: run the full test matrix in Testing an Extension Locally, including the vanilla-install check.