Contributing an Extension to the Registry
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.
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.
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
- Confirm eligibility against the acceptance policy above: open source, buildable with Maven, installs on vanilla.
- Fork
idempiere/idempiere-extension-repository. - Create
extensions/<your.bundle.symbolic.name>/. - Add
info.md, optionallyCHANGELOG.mdandassets/, and<version>/metadata.json, including a link to the public source repository and the commit or tag to build. - Open a pull request.
validate-extensions.ymlruns on the PR and checks everymetadata.jsonit can find.- Automated analysis runs, then a maintainer reviews. See What review looks like.
- The project builds the jar from your source and publishes it;
downloadUrlis set to that artifact. See How the jar is built and hosted. - On merge,
generate-index.ymlrunsscripts/generate_index.py, regeneratesindex.json, and commits it asgithub-actions[bot].
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.
| Field | Required | Rule | Enforced by |
|---|---|---|---|
id | Yes | Unique 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 |
version | Yes | OSGi 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 |
idempiereVersion | Yes | An OSGi version range tested against Adempiere.getVersion(). "14" means "14 and above"; use "[14,15)" to pin to a single major. | validateIDempiereVersion |
name | Yes | Shown in the extension list. | Validator |
bundles | Yes | Non-empty array. | Validator and CI |
bundles[].symbolicName | Yes | Must 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[].downloadUrl | See note below | Must parse as a URI. Points at the project-built artifact, not one you host. | CI requires it; the Java validator does not |
bundles[].sha256 | No | Hex SHA-256 of the project-built jar. Verified after download; a mismatch aborts the install. | installExtension |
releaseDate | No | Strict yyyy-MM-dd, non-lenient. 2026-13-01 is rejected. | Validator |
description | No | One line, shown under the name in the list. | Validator |
categories | No | String array, unique items. Filterable with category: in the form. | Validator |
tags | No | String array, unique items. Filterable with tag:. | Validator |
entityType | No | The 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[] | No | id 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.
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.mdorg.adempiere.replication/info.mdorg.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  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-Versionin step with the metadataversion. The review cross-checks them. - The
sha256is 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 withBundleSHA256HashNotMatch.
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.
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 withbundle.findEntries("META-INF", "2Pack_*.zip", false)after starting the bundle, andIncremental2PackActivatorimports 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_EntityTypeand use it for every record.
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 toIsActive='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.
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.mdif compatibility or configuration changed. - Bump the jar's
Bundle-Versionto 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.
- CI,
validate-extensions.ymlon the pull request. Checks required keys, non-emptybundles,symbolicName, anddownloadUrlper bundle. Fails fast, and tells you exactly which file and key. - Automated analysis, the
skills/idempiere-review-extensionchecklist, run over the source and the built artifact:- Documentation: the six
info.mdsections present, grammar, semantic accuracy, links resolve. - Schema:
metadata.jsonagainstmetadata-schema.json; mandatory fields, version pattern, valid URIs. - Bundles: the jar is actually downloaded; version and hash cross-checked against the metadata;
META-INFinspected; 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.
- Documentation: the six
- 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-SymbolicNamematchesbundles[].symbolicName, andBundle-Versionmatches the metadataversion. -
metadata.jsonhasid,name,version,idempiereVersion,bundles. -
downloadUrlandsha256agreed with the maintainers against the project-built artifact, not a binary you host. -
versionhas three numeric segments;releaseDateisyyyy-MM-dd. -
idempiereVersionrange actually matches the version you tested against. -
database[]lists all supported databases, or is omitted entirely. - Own
AD_EntityType; 2Pack namedMETA-INF/2Pack_<version>.zip.
Documentation
-
info.mdhas all six sections, including Source Code and Licence; every link resolves; images committed underassets/. -
CHANGELOG.mdupdated. -
index.jsonnot 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.
Related reading
- Testing an Extension Locally, test before you publish.
- Extension Registry Worked Example, a full submission from start to finish.
- Building iDempiere Plug-ins with Maven, setting up the required Maven/Tycho build.
- iDempiere Extension Management, the end-user overview of the feature.