codepapu
the playbook

10 October 2026 · 8 min read

CX Lab — every way to extend Liferay, in one workspace

An open-source Liferay workspace with one small, working example of every extension type: 26 OSGi modules and 28 client extensions, each with a README that says what it is, when to use it, and how to try it.


CX Lab is an open-source Liferay workspace with one small, working example of every way to extend the platform — 26 OSGi modules and 28 client extensions, side by side, each with its own README. Pick an extension point, read the README, deploy it.

What it is

A standard Liferay Workspace (Gradle, Java 21, Node 22) where every project is runnable on its own and named cx-lab-*, so anything you see in a log, a Gogo listing, or the Control Panel points straight back to one folder. Source and the v1.0 release live on GitHub: codepapu/liferay-cx-lab.

Why it exists

Liferay has more extension points than most teams ever touch, and the first question on every project is the same: module or client extension? Docs explain each type in isolation; what is missing is a place where they sit next to each other, working, so you can compare them on the same portal.

The fastest way to choose an extension type is to see both options running before you write a line of your own.

Two ways to extend

  • OSGi modules (modules/) run inside the portal JVM — full access to Liferay services, the permission checker, and the portal lifecycle.
  • Client extensions (client-extensions/) run outside it — deployed and upgraded on their own, talking to Liferay over Headless APIs and OAuth 2.

What's inside

OSGi modules

  • APIs and data — Service Builder with permissions and an MVC portlet, REST Builder (REST, GraphQL, OpenAPI, Java client), an API bundle with a swappable implementation, a cached stats service with ETag / 304, and a rate-limit + security-headers guard.
  • Sign-in— a custom login widget, dynamic includes on Liferay's own login page, and an authenticator / post-login / pre-logout pipeline.
  • Administration — a log viewer, a deployer, an SEO audit, a Job Scheduler task, Gogo commands, a model listener, and configuration at all four levels.
  • Content and search — web content structure and template, blogs widget template, fragments, a Search Blueprint, and a Kaleo workflow with Java action executors.
  • Language and Commerce — shared and overridden language keys, live JSP and localization guides, an offline bank transfer payment, and a store installer.

Client extensions

  • Browser — custom elements in plain JS and React + TypeScript, an import map entry, iframe, global JS and CSS, static content, CKEditor and Frontend Data Set extensions, and Audiences attributes.
  • Theme — a full theme CSS with style book tokens, a favicon, and an icon sprite map.
  • Configuration and data — instance settings as code, a batch that creates an Object, and a site initializer.
  • Microservices — a Spring Boot host for six server-side hooks (object action, validation, entry manager, workflow, notification, CAPTCHA), the same idea in Node.js, and a scheduled job.
  • Commerce — payment, shipping, tax, and checkout-step extensions.

Four configuration levels

One module adds a CX Lab category to the settings screens and one configuration per scope, so you can see exactly where each level is set and how Java reads it:

  • System — Control Panel → System Settings → getSystemConfiguration(…)
  • Instance — Control Panel → Instance Settings → getCompanyConfiguration(…, companyId)
  • Site — Site Settings → getGroupConfiguration(…, groupId)
  • Widget — the widget's Configuration dialog → getPortletInstanceConfiguration(…, themeDisplay)

One store, three ways

The same Commerce store — catalog, channel, warehouse, options, specifications, products, stock, and price list — is built three times, keyed by external reference code so each run is idempotent:

  1. Local services in an OSGi module, inside the portal.
  2. Batch client extension through the batch engine on deploy.
  3. Headless Commerce Admin APIs from a one-off job using OAuth 2.

The README contract

Every project answers the same five questions:

  1. What it is
  2. Why use it
  3. When to use it
  4. How it works
  5. How to try it

Ship it

Point the workspace at a portal (or let initBundle download one), then deploy one project or everything:

bash
git clone https://github.com/codepapu/liferay-cx-lab.git
cd liferay-cx-lab

./gradlew initBundle
bundles/tomcat/bin/catalina.sh run

./gradlew :modules:cx-lab-library:deploy
./gradlew :client-extensions:cx-lab-hello-tile:deploy
./gradlew deploy

Microservices deploy in two halves: deploy registers the yaml in the portal, then you run the service (bootRun for Spring Boot, yarn start for Node).


Takeaways

  • Compare extension types on the same portal before committing to one.
  • Small, single-purpose examples beat one giant demo you are afraid to touch.
  • Name everything so a log line leads straight back to its folder.
  • A README that explains when is worth more than one that only explains how.