<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <title>Liferay</title>
  <link rel="self" href="/c/blogs/rss" />
  <subtitle>Liferay</subtitle>
  <id>/c/blogs/rss</id>
  <updated>2026-09-12T15:29:42Z</updated>
  <dc:date>2026-09-12T15:29:42Z</dc:date>
  <entry>
    <title>Liferay DXP 2026.Q3 - Release Webinar - Replay</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126723763" />
    <author>
      <name>Ben Turner</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126723763</id>
    <updated>2026-09-02T01:01:24Z</updated>
    <published>2026-09-02T00:58:00Z</published>
    <summary type="html">&lt;p&gt;Hi everyone! I'm pleased to announce that the &lt;a
    href="https://www.liferay.com/web/lr/quarterly-release-webinar-q3-2026-recording"
    rel="noopener noreferrer" target="_blank"&gt;recording&lt;/a&gt; of the 2026.Q3 Release Webinar is now available for you to replay at your leisure!&lt;br&gt;
&lt;br&gt;
The recording includes details on key new features in the Q3 release like Design Libraries, CNE AWS Ready and Kubernetes Ready, as well as key new modules moving out of Beta status:&lt;br&gt;
•    Liferay Data Platform - GA&lt;br&gt;
•    Digital Sales Room - GA&lt;br&gt;
•    AI Hub - GA&lt;br&gt;
•    Content Marketing Platform - Release&lt;/p&gt;





&lt;h4&gt;For more details and a link to the recording and PDF you can visit:&lt;/h4&gt;





&lt;p&gt;&lt;a
    href="https://learn.liferay.com/d/liferay-dxp-2026-q2-release-webinar/"
    rel="noopener noreferrer" target="_blank"&gt;https://www.liferay.com/web/lr/quarterly-release-webinar-q3-2026-recording&lt;/a&gt;&lt;/p&gt;





&lt;p&gt;Any questions, you know where to find me!&lt;/p&gt;





&lt;p&gt;All the best,&lt;/p&gt;





&lt;p&gt;Ben&lt;/p&gt;</summary>
    <dc:creator>Ben Turner</dc:creator>
    <dc:date>2026-09-02T00:58:00Z</dc:date>
  </entry>
  <entry>
    <title>Enabling ModSecurity in Liferay PaaS</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126640680" />
    <author>
      <name>David H Nebinger</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126640680</id>
    <updated>2026-08-18T19:21:59Z</updated>
    <published>2026-08-18T18:45:00Z</published>
    <summary type="html">&lt;p&gt;Liferay's documentation does a good job of explaining how to turn ModSecurity on, how to provide your own configuration, and how to send the audit log to the Liferay Cloud Console.&lt;/p&gt;





&lt;p&gt;But, as I recently discovered while working with a client, there is a pretty big gap between:&lt;/p&gt;





&lt;blockquote&gt;
&lt;p&gt;ModSecurity is enabled and I can see logs.&lt;/p&gt;
&lt;/blockquote&gt;





&lt;p&gt;and:&lt;/p&gt;





&lt;blockquote&gt;
&lt;p&gt;I'm getting the ModSecurity information I actually need, reliably, without overwhelming the logging pipeline.&lt;/p&gt;
&lt;/blockquote&gt;





&lt;p&gt;We spent quite a bit of time working through that gap.&lt;/p&gt;





&lt;p&gt;This post isn't intended to replace the official documentation. Think of it as the next chapter: some practical configuration guidance that became apparent only after working through a real-world implementation.&lt;/p&gt;





&lt;h2&gt;Start with the Liferay Documentation&lt;/h2&gt;





&lt;p&gt;If you haven't already enabled ModSecurity, start with &lt;a
    href="https://learn.liferay.com/w/dxp/cloud/tuning-security-settings/web-application-firewall" rel="noopener noreferrer"&gt;Liferay's official Web Application Firewall documentation&lt;/a&gt;.&lt;/p&gt;





&lt;p&gt;In &lt;code&gt;webserver/LCP.json&lt;/code&gt;, enable ModSecurity using:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;{
    &amp;quot;env&amp;quot;: {
        &amp;quot;LCP_WEBSERVER_MODSECURITY&amp;quot;: &amp;quot;DetectionOnly&amp;quot;
    }
}&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Liferay supports three values:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;code&gt;Off&lt;/code&gt; — ModSecurity does not process rules.&lt;/li&gt;
	&lt;li&gt;&lt;code&gt;DetectionOnly&lt;/code&gt; — rules are evaluated, but disruptive actions aren't executed.&lt;/li&gt;
	&lt;li&gt;&lt;code&gt;On&lt;/code&gt; — rules are evaluated and enforcement actions can be executed.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;Liferay recommends starting with &lt;code&gt;DetectionOnly&lt;/code&gt;, testing and tuning the rules, and moving to &lt;code&gt;On&lt;/code&gt; only after you're comfortable that legitimate traffic won't be disrupted.&lt;/p&gt;





&lt;p&gt;That's good advice. Don't skip that step.&lt;/p&gt;





&lt;p&gt;Also remember that if you provide your own:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;webserver/configs/[ENV]/modsec/modsecurity.conf&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;you're not augmenting the default Liferay configuration.&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;You're replacing it.&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt;Your file needs to provide all of the necessary ModSecurity configuration. That detail becomes important very quickly.&lt;/p&gt;





&lt;h2&gt;Local Log Files and Containers Don't Mix Particularly Well&lt;/h2&gt;





&lt;p&gt;The default audit-log configuration typically writes to something like:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;/var/log/modsec_audit.log&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;That's perfectly reasonable on a traditional server.&lt;/p&gt;





&lt;p&gt;The server starts, it runs for months or years, and logs accumulate on its filesystem until something rotates or archives them.&lt;/p&gt;





&lt;p&gt;A PaaS container doesn't work that way.&lt;/p&gt;





&lt;p&gt;Containers can be replaced, restarted, rescheduled, or scaled. A log file living only inside one webserver container belongs to the lifetime of that container.&lt;/p&gt;





&lt;p&gt;So if the goal is to retain ModSecurity output independently of a particular container, writing only to a local file isn't a very useful long-term strategy.&lt;/p&gt;





&lt;p&gt;Liferay's documentation already provides the key change:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditLog /dev/stdout&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;This sends the audit output to standard output, allowing it to appear with the webserver service logs in the Liferay Cloud Console making it part of the platform logging stream, but more importantly it means you get ModSec logs from all containers (even those that autoscale up/down) and can download/retain them for as long as you require.&lt;/p&gt;





&lt;p&gt;But this is also where things get interesting.&lt;/p&gt;





&lt;h2&gt;&lt;code&gt;/dev/stdout&lt;/code&gt; Doesn't Mean &amp;quot;Dump Everything&amp;quot;&lt;/h2&gt;





&lt;p&gt;During our troubleshooting, we reached a configuration that looked approximately like this:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditEngine On

SecAuditLogParts ABIJDEFHZK

SecAuditLogType Serial
SecAuditLog /dev/stdout
SecAuditLogFormat JSON&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;At first glance, that seems reasonable.&lt;/p&gt;





&lt;p&gt;Turn auditing on.&lt;/p&gt;





&lt;p&gt;Capture lots of information.&lt;/p&gt;





&lt;p&gt;Use JSON because structured logging is cool.&lt;/p&gt;





&lt;p&gt;Send it all to stdout.&lt;/p&gt;





&lt;p&gt;What could possibly go wrong?&lt;/p&gt;





&lt;p&gt;Quite a lot, actually.&lt;/p&gt;





&lt;h2&gt;&lt;code&gt;SecAuditEngine On&lt;/code&gt; Can Generate a Lot of Data&lt;/h2&gt;





&lt;p&gt;&lt;code&gt;SecAuditEngine&lt;/code&gt; controls which transactions receive an audit record.&lt;/p&gt;





&lt;p&gt;The available values are:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditEngine Off
SecAuditEngine On
SecAuditEngine RelevantOnly&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;&lt;code&gt;On&lt;/code&gt; means &lt;strong&gt;audit every transaction&lt;/strong&gt;.&lt;/p&gt;





&lt;p&gt;&lt;code&gt;RelevantOnly&lt;/code&gt; means audit transactions that generated a warning or error, or whose response status matches &lt;code&gt;SecAuditLogRelevantStatus&lt;/code&gt;.&lt;/p&gt;





&lt;p&gt;For a security audit log, logging every ordinary request usually isn't what you're looking for.&lt;/p&gt;





&lt;p&gt;A normal page can involve dozens or hundreds of requests. Add images, JavaScript, CSS, APIs, probes, monitoring requests, and normal application traffic, and &lt;code&gt;On&lt;/code&gt; can generate an enormous amount of audit data.&lt;/p&gt;





&lt;p&gt;Our working configuration changed this to:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditEngine RelevantOnly&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;For example:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus &amp;quot;^(?:5|4(?!04))&amp;quot;&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;For a WAF, that's much closer to what you normally want:&lt;/p&gt;





&lt;blockquote&gt;
&lt;p&gt;Tell me about the interesting transactions. Don't create another copy of my entire access log.&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h2&gt;Then There Was the &lt;code&gt;E&lt;/code&gt;&lt;/h2&gt;





&lt;p&gt;The second major finding was buried inside this innocent-looking setting:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditLogParts ABIJDEFHZK&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Those letters aren't a format.&lt;/p&gt;





&lt;p&gt;They tell ModSecurity &lt;strong&gt;which parts of the HTTP transaction to include in the audit record&lt;/strong&gt;.&lt;/p&gt;





&lt;p&gt;And one of those letters can make a very big difference:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;E&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;&lt;code&gt;E&lt;/code&gt; tells ModSecurity to include the &lt;strong&gt;response body&lt;/strong&gt;.&lt;/p&gt;





&lt;p&gt;Think about what that means.&lt;/p&gt;





&lt;p&gt;If the response is a 300 KB HTML page, you can end up putting that response into the audit record.&lt;/p&gt;





&lt;p&gt;Now combine:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditEngine On&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;with:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditLogParts ...E...&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;and:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditLogFormat JSON&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;and:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditLog /dev/stdout&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;You've potentially asked ModSecurity to serialize large response bodies into JSON for every request and push all of it through the container logging pipeline.&lt;/p&gt;





&lt;p&gt;That turned out to be a very important combination in our troubleshooting.&lt;/p&gt;





&lt;p&gt;The change that ultimately helped was removing &lt;code&gt;E&lt;/code&gt;:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;- SecAuditLogParts ABIJDEFHZK
+ SecAuditLogParts ABIJDFHZK&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Same auditing capability.&lt;/p&gt;





&lt;p&gt;Same rules.&lt;/p&gt;





&lt;p&gt;Same request processing.&lt;/p&gt;





&lt;p&gt;But the response body is no longer being stuffed into the audit output.&lt;/p&gt;





&lt;p&gt;And that's an important distinction:&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Removing &lt;code&gt;E&lt;/code&gt; does not disable response-body inspection.&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt;&lt;code&gt;SecResponseBodyAccess&lt;/code&gt; controls whether ModSecurity examines response bodies.&lt;/p&gt;





&lt;p&gt;&lt;code&gt;SecAuditLogParts&lt;/code&gt; controls what gets &lt;strong&gt;recorded in the audit log&lt;/strong&gt;.&lt;/p&gt;





&lt;p&gt;Those are two different decisions.&lt;/p&gt;





&lt;h2&gt;So What Do All Those Letters Mean?&lt;/h2&gt;





&lt;p&gt;Rather than copying a value from a sample configuration, you should understand what you're asking ModSecurity to retain.&lt;/p&gt;





&lt;h3&gt;&lt;code&gt;A&lt;/code&gt; — Audit Log Header&lt;/h3&gt;





&lt;p&gt;Contains the audit entry header. This part is mandatory.&lt;/p&gt;





&lt;h3&gt;&lt;code&gt;B&lt;/code&gt; — Request Headers&lt;/h3&gt;





&lt;p&gt;Contains the HTTP request headers. This is normally very useful when investigating why a rule matched.&lt;/p&gt;





&lt;h3&gt;&lt;code&gt;C&lt;/code&gt; — Request Body&lt;/h3&gt;





&lt;p&gt;Contains the request body when one exists and request-body inspection is enabled.&lt;/p&gt;





&lt;p&gt;This can be useful, but request bodies can also contain substantial amounts of data or sensitive information.&lt;/p&gt;





&lt;h3&gt;&lt;code&gt;D&lt;/code&gt; — Reserved&lt;/h3&gt;





&lt;p&gt;Reserved for intermediary response headers. It is not currently implemented in ModSecurity 3.x.&lt;/p&gt;





&lt;h3&gt;&lt;code&gt;E&lt;/code&gt; — Response Body&lt;/h3&gt;





&lt;p&gt;Contains the response body when response-body inspection is enabled.&lt;/p&gt;





&lt;p&gt;This was the important one in our investigation.&lt;/p&gt;





&lt;p&gt;Response bodies can be &lt;strong&gt;large&lt;/strong&gt;, and including them in centralized audit output can dramatically increase both individual record size and overall log volume.&lt;/p&gt;





&lt;p&gt;Unless you have a specific business or forensic requirement for storing response bodies, I would leave &lt;code&gt;E&lt;/code&gt; out.&lt;/p&gt;





&lt;h3&gt;&lt;code&gt;F&lt;/code&gt; — Final Response Headers&lt;/h3&gt;





&lt;p&gt;Contains the final response headers. Useful without carrying the potentially large cost of storing the response body.&lt;/p&gt;





&lt;h3&gt;&lt;code&gt;G&lt;/code&gt; — Reserved&lt;/h3&gt;





&lt;p&gt;Reserved for the actual response body and not currently implemented.&lt;/p&gt;





&lt;h3&gt;&lt;code&gt;H&lt;/code&gt; — Audit Log Trailer&lt;/h3&gt;





&lt;p&gt;Contains the audit-log trailer and additional transaction information.&lt;/p&gt;





&lt;h3&gt;&lt;code&gt;I&lt;/code&gt; — Not Implemented in ModSecurity 3.x&lt;/h3&gt;





&lt;p&gt;Older ModSecurity documentation describes &lt;code&gt;I&lt;/code&gt; as a reduced multipart request body that could be used instead of &lt;code&gt;C&lt;/code&gt;.&lt;/p&gt;





&lt;p&gt;The ModSecurity 3.x reference lists it as not implemented.&lt;/p&gt;





&lt;h3&gt;&lt;code&gt;J&lt;/code&gt; — Uploaded File Information&lt;/h3&gt;





&lt;p&gt;Contains information about files uploaded using &lt;code&gt;multipart/form-data&lt;/code&gt;.&lt;/p&gt;





&lt;h3&gt;&lt;code&gt;K&lt;/code&gt; — Not Implemented in ModSecurity 3.x&lt;/h3&gt;





&lt;p&gt;In older ModSecurity versions, &lt;code&gt;K&lt;/code&gt; represented matched-rule information. In ModSecurity 3.x it is listed as not implemented.&lt;/p&gt;





&lt;h3&gt;&lt;code&gt;Z&lt;/code&gt; — End of Audit Entry&lt;/h3&gt;





&lt;p&gt;Marks the end of the audit entry. Like &lt;code&gt;A&lt;/code&gt;, this is mandatory.&lt;/p&gt;





&lt;h2&gt;Choose the Parts You Actually Need&lt;/h2&gt;





&lt;p&gt;The ModSecurity 3.x documented default is:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;ABCFHZ&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;The configuration we were working with contained:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;ABIJDEFHZK&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;and the critical operational change was:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;ABIJDFHZK&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;removing &lt;code&gt;E&lt;/code&gt;.&lt;/p&gt;





&lt;p&gt;If I were designing a brand-new configuration today, though, I wouldn't start by asking:&lt;/p&gt;





&lt;blockquote&gt;
&lt;p&gt;What long sequence of letters should I copy?&lt;/p&gt;
&lt;/blockquote&gt;





&lt;p&gt;I'd ask:&lt;/p&gt;





&lt;blockquote&gt;
&lt;p&gt;What information does the security team actually need?&lt;/p&gt;
&lt;/blockquote&gt;





&lt;p&gt;Then select the parts accordingly.&lt;/p&gt;





&lt;p&gt;In particular, make deliberate decisions about:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;C - request body
E - response body
J - uploaded file information&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Those can materially affect log size and potentially the sensitivity of the information you're retaining.&lt;/p&gt;





&lt;h2&gt;Native or JSON?&lt;/h2&gt;





&lt;p&gt;This is another place where two different settings are sometimes conflated.&lt;/p&gt;





&lt;p&gt;The audit &lt;strong&gt;parts&lt;/strong&gt; are selected with:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditLogParts ...&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;The audit &lt;strong&gt;format&lt;/strong&gt; is selected separately:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditLogFormat Native&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;or:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditLogFormat JSON&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;The default ModSecurity format is &lt;code&gt;Native&lt;/code&gt;. JSON is also supported.&lt;/p&gt;





&lt;p&gt;JSON can be very useful.&lt;/p&gt;





&lt;p&gt;If you're feeding logs into a SIEM, writing automated analysis, or extracting rule IDs, anomaly scores, messages, and transaction fields programmatically, JSON is much easier to work with.&lt;/p&gt;





&lt;p&gt;But JSON isn't required to make ModSecurity logging work.&lt;/p&gt;





&lt;p&gt;My recommendation is to make this a &lt;strong&gt;business/use-case decision&lt;/strong&gt;, not a technical checkbox.&lt;/p&gt;





&lt;h3&gt;Use Native when:&lt;/h3&gt;





&lt;ul&gt;
	&lt;li&gt;humans are primarily reading the logs;&lt;/li&gt;
	&lt;li&gt;you want the familiar ModSecurity audit representation;&lt;/li&gt;
	&lt;li&gt;you don't need structured downstream processing.&lt;/li&gt;
&lt;/ul&gt;





&lt;h3&gt;Consider JSON when:&lt;/h3&gt;





&lt;ul&gt;
	&lt;li&gt;you're feeding the records into security tooling;&lt;/li&gt;
	&lt;li&gt;you're building automated filtering or analytics;&lt;/li&gt;
	&lt;li&gt;you need structured fields rather than text parsing.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;But regardless of the format, control what you're putting into each record.&lt;/p&gt;





&lt;p&gt;Changing from Native to JSON doesn't make a huge response body suddenly small.&lt;/p&gt;





&lt;h2&gt;Don't Forget the Other ModSecurity Log&lt;/h2&gt;





&lt;p&gt;One thing that caused some confusion during our work was that ModSecurity information can show up through more than one logging path.&lt;/p&gt;





&lt;p&gt;The detailed transaction-oriented information goes to the &lt;strong&gt;audit log&lt;/strong&gt;.&lt;/p&gt;





&lt;p&gt;But the familiar messages that look something like:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;ModSecurity: Warning. Matched ...&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;are emitted through Nginx's error logging path.&lt;/p&gt;





&lt;p&gt;So if those are the messages you care about seeing in the Liferay Console, make sure Nginx is also writing its error log to stdout:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;error_log /dev/stdout warn;&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Nginx supports these logging levels:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;debug
info
notice
warn
error
crit
alert
emerg&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Selecting a level includes that level and all more-severe levels.&lt;/p&gt;





&lt;p&gt;In our case, &lt;code&gt;warn&lt;/code&gt; was the right normal operating level.&lt;/p&gt;





&lt;p&gt;During troubleshooting you might temporarily use &lt;code&gt;notice&lt;/code&gt;, &lt;code&gt;info&lt;/code&gt;, or even &lt;code&gt;debug&lt;/code&gt;.&lt;/p&gt;





&lt;p&gt;But don't leave highly verbose logging enabled just because more data feels safer.&lt;/p&gt;





&lt;p&gt;Sometimes more logs just means more hay around the needle.&lt;/p&gt;





&lt;h2&gt;Be Careful with &lt;code&gt;SecDebugLogLevel 9&lt;/code&gt;&lt;/h2&gt;





&lt;p&gt;Along the same lines, we used very verbose ModSecurity debug logging while troubleshooting.&lt;/p&gt;





&lt;p&gt;That's appropriate when you're trying to understand why something isn't behaving correctly.&lt;/p&gt;





&lt;p&gt;It isn't something I want running indefinitely.&lt;/p&gt;





&lt;p&gt;For normal operation we brought the level back down:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecDebugLogLevel 3&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;rather than:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecDebugLogLevel 9&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Level 9 can be useful when troubleshooting, but once you understand the problem, turn the noise back down.&lt;/p&gt;





&lt;h2&gt;A Practical PaaS Configuration&lt;/h2&gt;





&lt;p&gt;Pulling the important pieces together, the configuration pattern I'd start from looks more like this:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecRuleEngine ${LCP_WEBSERVER_MODSECURITY}

SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus &amp;quot;^(?:5|4(?!04))&amp;quot;

SecAuditLogParts ABIJDFHZK

SecAuditLogType Serial
SecAuditLog /dev/stdout

SecDebugLogLevel 3&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;And in Nginx:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;error_log /dev/stdout warn;&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Then make an explicit decision about the audit format.&lt;/p&gt;





&lt;p&gt;For Native:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditLogFormat Native&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;or simply rely on the native default.&lt;/p&gt;





&lt;p&gt;For structured output:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditLogFormat JSON&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;And again, review &lt;code&gt;SecAuditLogParts&lt;/code&gt; rather than simply copying my string.&lt;/p&gt;





&lt;p&gt;The important operational characteristics are:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;SecAuditEngine RelevantOnly&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;rather than &lt;code&gt;On&lt;/code&gt;, and:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;E is not included in SecAuditLogParts&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;unless you have a deliberate reason for retaining response bodies.&lt;/p&gt;





&lt;h2&gt;What About Access Logs?&lt;/h2&gt;





&lt;p&gt;You may also want the normal Nginx request record in the same centralized stream:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;access_log /dev/stdout main;&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;That can be helpful when correlating a ModSecurity event back to the corresponding request.&lt;/p&gt;





&lt;p&gt;Whether you want all access-log traffic in the Console is a separate operational decision.&lt;/p&gt;





&lt;p&gt;Again, resist the temptation to turn on everything simply because you can.&lt;/p&gt;





&lt;h2&gt;And Then Download the Logs&lt;/h2&gt;





&lt;p&gt;Getting the logs into the Liferay Console solves the container-lifetime problem, but the Console shouldn't necessarily be your organization's permanent audit archive, especially since logs are only kept for 30 days.&lt;/p&gt;





&lt;p&gt;The &lt;a
    href="https://learn.liferay.com/w/liferay-cloud/reference/command-line-tool" rel="noopener noreferrer"&gt;Liferay command-line tool&lt;/a&gt; can retrieve service logs:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;lcp log -p &amp;lt;project&amp;gt;-&amp;lt;environment&amp;gt; -s webserver&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;and constrain the export by time:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;lcp log \
    -p &amp;lt;project&amp;gt;-&amp;lt;environment&amp;gt; \
    -s webserver \
    --since &amp;quot;&amp;lt;start_time&amp;gt;&amp;quot; \
    --until &amp;quot;&amp;lt;end_time&amp;gt;&amp;quot; \
    &amp;gt;&amp;gt; webserver-logs.txt&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;For an organization that needs longer-term security retention, I'd automate this.&lt;/p&gt;





&lt;p&gt;For example, create a scheduled job to download the logs once a week, filter out the log messages not related to ModSec, and then keep those files for as long as you need.&lt;/p&gt;





&lt;p&gt;If you're using JSON audit output, that processing gets particularly easy because you can parse fields instead of relying entirely on string matching.&lt;/p&gt;





&lt;h2&gt;The Bigger Lesson&lt;/h2&gt;





&lt;p&gt;The biggest lesson from this exercise wasn't a magic ModSecurity directive.&lt;/p&gt;





&lt;p&gt;It was that logging in a container platform needs to be treated as a &lt;strong&gt;pipeline&lt;/strong&gt;, not as a file.&lt;/p&gt;





&lt;p&gt;There's the application generating the information.&lt;/p&gt;





&lt;p&gt;There's ModSecurity deciding what should be audited.&lt;/p&gt;





&lt;p&gt;There's Nginx deciding what goes to its error and access logs.&lt;/p&gt;





&lt;p&gt;There's stdout.&lt;/p&gt;





&lt;p&gt;There's the container runtime.&lt;/p&gt;





&lt;p&gt;There's the platform logging infrastructure.&lt;/p&gt;





&lt;p&gt;And finally there's the Console where a human sees the result.&lt;/p&gt;





&lt;p&gt;You can have a perfectly valid ModSecurity configuration and still create a terrible logging configuration.&lt;/p&gt;





&lt;p&gt;In our case, these were all individually reasonable choices:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;Audit everything.&lt;/li&gt;
	&lt;li&gt;Include the response.&lt;/li&gt;
	&lt;li&gt;Use JSON.&lt;/li&gt;
	&lt;li&gt;Send it to stdout.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;Together, they weren't reasonable.&lt;/p&gt;





&lt;p&gt;The final approach was much simpler:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;Audit the relevant transactions.&lt;/li&gt;
	&lt;li&gt;Don't include giant response bodies unless you actually need them.&lt;/li&gt;
	&lt;li&gt;Send the useful output to stdout.&lt;/li&gt;
	&lt;li&gt;Keep normal logging at a sensible verbosity.&lt;/li&gt;
	&lt;li&gt;Export what you need for long-term retention.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;That's a configuration you can live with.&lt;/p&gt;





&lt;p&gt;And, more importantly, it's one the logging pipeline can live with too.&lt;/p&gt;</summary>
    <dc:creator>David H Nebinger</dc:creator>
    <dc:date>2026-08-18T18:45:00Z</dc:date>
  </entry>
  <entry>
    <title>When a CVE Isn't a Liferay Vulnerability</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126625355" />
    <author>
      <name>David H Nebinger</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126625355</id>
    <updated>2026-08-17T16:56:54Z</updated>
    <published>2026-08-16T19:22:00Z</published>
    <summary type="html">&lt;p&gt;Security scanners are very good at finding things.&lt;/p&gt;





&lt;p&gt;Run one against a Liferay DXP installation and it will inventory third-party libraries, determine their versions, compare those versions against vulnerability databases such as the NVD, and produce a list of CVEs.&lt;/p&gt;





&lt;p&gt;Then somebody opens a Liferay Support ticket.&lt;/p&gt;





&lt;blockquote&gt;
&lt;p&gt;We're running version X of library Y. CVE-Z says that version is vulnerable. When will Liferay upgrade it?&lt;/p&gt;
&lt;/blockquote&gt;





&lt;p&gt;Sometimes the answer is exactly what you would expect: Liferay upgrades the library.&lt;/p&gt;





&lt;p&gt;But quite often the answer is something closer to:&lt;/p&gt;





&lt;blockquote&gt;
&lt;p&gt;Liferay DXP is not affected by this vulnerability.&lt;/p&gt;
&lt;/blockquote&gt;





&lt;p&gt;That answer can be frustrating, particularly when the scanner has assigned the finding a High or Critical severity. The JAR is there. The version matches. The CVE exists. How can Liferay say it isn't vulnerable?&lt;/p&gt;





&lt;p&gt;Because those are actually three different statements.&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;The library is present.&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;A vulnerability exists in that version of the library.&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;That vulnerability can be exploited through Liferay DXP.&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt;A security scanner can usually establish the first two. Establishing the third requires understanding how the software actually works.&lt;/p&gt;





&lt;p&gt;And that is where things get interesting.&lt;/p&gt;





&lt;h2&gt;The Scanner Isn't Wrong&lt;/h2&gt;





&lt;p&gt;Let's start by giving the scanner some credit.&lt;/p&gt;





&lt;p&gt;If the scanner says that Liferay contains a particular version of a third-party library and that version is associated with a CVE, it may be completely correct.&lt;/p&gt;





&lt;p&gt;The mistake is treating that information as proof that the resulting Liferay installation is exploitable.&lt;/p&gt;





&lt;p&gt;Software Composition Analysis tools generally work from something resembling this model:&lt;code&gt; &lt;/code&gt;&lt;/p&gt;





&lt;p&gt;&lt;code&gt;&lt;img data-fileentryid="126625365" src="https://liferay.dev/documents/portlet_file_entry/14/sca-tools.png/2b53684c-cd2e-17dc-31ec-9350616f158c"&gt;&lt;/code&gt;&lt;/p&gt;





&lt;p&gt;That's useful information, but it is only the beginning of vulnerability analysis.&lt;/p&gt;





&lt;p&gt;A CVE usually applies to some particular behavior within a library. Exploiting it may require a certain class to be used, a particular feature to be enabled, a specific configuration, attacker-controlled input to reach a particular method, or some combination of all of these.&lt;/p&gt;





&lt;p&gt;If the application never exposes that attack surface, the vulnerable code can physically exist inside the JAR without providing an exploitable path through the application.&lt;/p&gt;





&lt;p&gt;The scanner has identified a &lt;strong&gt;potential vulnerability&lt;/strong&gt;.&lt;/p&gt;





&lt;p&gt;Now someone has to determine whether it is a &lt;strong&gt;reachable vulnerability&lt;/strong&gt;.&lt;/p&gt;





&lt;h2&gt;Spring Is a Good Example&lt;/h2&gt;





&lt;p&gt;Consider Spring 6.2.9 as an example.&lt;/p&gt;





&lt;p&gt;There are several CVEs associated with Spring components around these versions, including:&lt;/p&gt;





&lt;table border="1" cellpadding="1" cellspacing="1"&gt;
	&lt;thead&gt;
		&lt;tr&gt;
			&lt;th&gt;CVE&lt;/th&gt;
			&lt;th&gt;Area&lt;/th&gt;
			&lt;th&gt;Important Exploitation Condition&lt;/th&gt;
		&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;CVE-2025-41242&lt;/td&gt;
			&lt;td&gt;Spring MVC path traversal&lt;/td&gt;
			&lt;td&gt;Requires particular Spring static-resource handling combined with a Servlet container that does not reject the malicious path sequences.&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;CVE-2025-22228&lt;/td&gt;
			&lt;td&gt;Spring Security &lt;code&gt;BCryptPasswordEncoder&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;Requires use of the affected password encoder behavior with passwords longer than 72 characters.&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;CVE-2025-41249&lt;/td&gt;
			&lt;td&gt;Spring annotation detection&lt;/td&gt;
			&lt;td&gt;Relevant when Spring Security method security is enabled and authorization annotations occur in particular parameterized type hierarchies.&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;





&lt;p&gt;These aren't hypothetical qualifications added by Liferay. They come directly from Spring's own security advisories. For example, Spring says CVE-2025-41242 depends on several conditions and specifically notes that default Tomcat and Jetty security behavior prevents the vulnerability. CVE-2025-41249 similarly identifies &lt;code&gt;@EnableMethodSecurity&lt;/code&gt; as part of the affected scenario.&lt;/p&gt;





&lt;p&gt;CVE-2025-22228 affects &lt;code&gt;BCryptPasswordEncoder.matches()&lt;/code&gt; when passwords exceed 72 characters and the first 72 characters match. Again, that is a real vulnerability, but an application has to actually use that Spring Security functionality in an exploitable authentication path before it becomes an application vulnerability.&lt;/p&gt;





&lt;p&gt;And who knows? By the time you're reading this there may be several more CVEs associated with these versions.&lt;/p&gt;





&lt;p&gt;That's the nature of modern software.&lt;/p&gt;





&lt;p&gt;What matters is what happens next.&lt;/p&gt;





&lt;h2&gt;Presence Is Not the Same as Exploitability&lt;/h2&gt;





&lt;p&gt;When Liferay's application security team evaluates one of these reports, the CVE number and library version are just the starting point.&lt;/p&gt;





&lt;p&gt;The real questions look more like this:&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;What exactly is the vulnerable code?&lt;/li&gt;
	&lt;li&gt;What conditions are required to exploit it?&lt;/li&gt;
	&lt;li&gt;Does Liferay use that code?&lt;/li&gt;
	&lt;li&gt;Is the vulnerable capability enabled?&lt;/li&gt;
	&lt;li&gt;Can an attacker reach it?&lt;/li&gt;
	&lt;li&gt;Can attacker-controlled input reach the vulnerable path?&lt;/li&gt;
	&lt;li&gt;Does Liferay's own configuration or surrounding architecture prevent the exploit?&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;Only after answering those questions can you determine the actual risk to Liferay DXP.&lt;/p&gt;





&lt;p&gt;This concept is often called &lt;strong&gt;reachability&lt;/strong&gt;.&lt;/p&gt;





&lt;p&gt;Imagine a third-party JAR containing 5,000 classes. One method in one class contains a vulnerability.&lt;/p&gt;





&lt;p&gt;If Liferay never loads that class, never invokes that method, never enables the associated feature, and provides no path for attacker-controlled input to reach it, replacing the entire JAR may change the version number reported by your scanner without materially improving the security of the platform.&lt;/p&gt;





&lt;p&gt;That's an important distinction.&lt;/p&gt;





&lt;p&gt;Security isn't about achieving the highest possible dependency version number.&lt;/p&gt;





&lt;p&gt;It's about eliminating exploitable attack paths.&lt;/p&gt;





&lt;h2&gt;What Happens When Liferay Investigates a CVE?&lt;/h2&gt;





&lt;p&gt;Liferay has a dedicated application security team continuously reviewing vulnerabilities reported against both Liferay code and the third-party software included with DXP.&lt;/p&gt;





&lt;p&gt;Often the CVE has already been investigated before a customer reports it through Support.&lt;/p&gt;





&lt;p&gt;The analysis goes deeper than the CVE headline.&lt;/p&gt;





&lt;p&gt;The team looks at what causes the vulnerability, what is required to exploit it, and how a working exploit would behave. Then they compare those requirements against the way the affected library is actually being used by Liferay DXP.&lt;/p&gt;





&lt;p&gt;The team also leverages tools like &lt;a href="https://konvu.com" rel="noopener noreferrer"&gt;Konvu&lt;/a&gt; and other AI tools during the analysis to ensure it's not a cursory review.&lt;/p&gt;





&lt;div
    class="overflow-auto portlet-msg-info"&gt;You can read more about CVEs the security team has reviewed along with their classifications here: &lt;a
    href="https://support.liferay.com/security-vulnerabilities" rel="noopener noreferrer"&gt;https://support.liferay.com/security-vulnerabilities&lt;/a&gt;.&lt;/div&gt;





&lt;p&gt;Sometimes that analysis concludes that Liferay is affected.&lt;/p&gt;





&lt;p&gt;Sometimes it concludes that Liferay is not affected.&lt;/p&gt;





&lt;p&gt;There are even cases where the answer depends on configuration.&lt;/p&gt;





&lt;p&gt;That's exactly the kind of application-level context that a simple dependency/version scan cannot provide.&lt;/p&gt;





&lt;h2&gt;The Log4j 1 Lesson&lt;/h2&gt;





&lt;p&gt;Anyone who dealt with enterprise security scanning during the later years of Log4j 1 probably remembers this problem particularly well.&lt;/p&gt;





&lt;p&gt;Log4j 1 reached end of life, but applications continued using it for years afterward. As vulnerabilities were discovered, scanners accumulated an increasingly alarming collection of findings.&lt;/p&gt;





&lt;p&gt;Some of them were severe.&lt;/p&gt;





&lt;table border="1" cellpadding="1" cellspacing="1"&gt;
	&lt;thead&gt;
		&lt;tr&gt;
			&lt;th&gt;CVE&lt;/th&gt;
			&lt;th&gt;Component&lt;/th&gt;
			&lt;th&gt;Attack Surface&lt;/th&gt;
		&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;CVE-2019-17571&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;SocketServer&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;Deserialization through the Log4j network logging server.&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;CVE-2021-4104&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;JMSAppender&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;JNDI behavior associated with JMS logging configuration.&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;CVE-2022-23302&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;JMSSink&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;JNDI and deserialization behavior through the JMS sink.&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;CVE-2022-23305&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;JDBCAppender&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;SQL injection through the database logging appender.&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;CVE-2022-23307&lt;/td&gt;
			&lt;td&gt;Chainsaw&lt;/td&gt;
			&lt;td&gt;Deserialization in the Chainsaw logging viewer.&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;





&lt;p&gt;These vulnerabilities were real. Nobody was disputing that.&lt;/p&gt;





&lt;p&gt;But look at the affected functionality.&lt;/p&gt;





&lt;p&gt;Liferay wasn't running a Log4j &lt;code&gt;SocketServer&lt;/code&gt;.&lt;/p&gt;





&lt;p&gt;It wasn't using Chainsaw.&lt;/p&gt;





&lt;p&gt;It wasn't using the &lt;code&gt;JDBCAppender&lt;/code&gt;.&lt;/p&gt;





&lt;p&gt;It wasn't using the vulnerable JMS logging components.&lt;/p&gt;





&lt;p&gt;There was no route through a normal Liferay DXP installation for an attacker to reach those vulnerable features.&lt;/p&gt;





&lt;p&gt;That didn't stop scanners from finding them.&lt;/p&gt;





&lt;p&gt;From the scanner's perspective, everything was working exactly as designed:&lt;code&gt; &lt;/code&gt;&lt;/p&gt;





&lt;p&gt;&lt;code&gt;&lt;img data-fileentryid="126625376" src="https://liferay.dev/documents/portlet_file_entry/14/log4j1.png/0e45cbd7-a1bd-b6a2-177b-07c5f0cc09e0"&gt;&lt;/code&gt;&lt;/p&gt;





&lt;p&gt;The scanner had no idea whether Liferay was using &lt;code&gt;JDBCAppender&lt;/code&gt;, &lt;code&gt;SocketServer&lt;/code&gt;, or anything else inside the library.&lt;/p&gt;





&lt;p&gt;It only knew the library was there.&lt;/p&gt;





&lt;h2&gt;But Sometimes &amp;quot;Not Vulnerable&amp;quot; Isn't Enough&lt;/h2&gt;





&lt;p&gt;This creates a practical problem.&lt;/p&gt;





&lt;p&gt;A Critical CVE sitting on a security report is difficult for many organizations to accept, even when the vendor has analyzed it and determined that the product is not affected.&lt;/p&gt;





&lt;p&gt;Security policy might say every Critical CVE must be eliminated.&lt;/p&gt;





&lt;p&gt;An auditor might require remediation.&lt;/p&gt;





&lt;p&gt;A customer's vulnerability management platform may refuse to close the finding.&lt;/p&gt;





&lt;p&gt;And eventually someone asks:&lt;/p&gt;





&lt;blockquote&gt;
&lt;p&gt;Why doesn't Liferay just upgrade the library and make the scanner happy?&lt;/p&gt;
&lt;/blockquote&gt;





&lt;p&gt;The Log4j 1 situation became a good example of how this can play out.&lt;/p&gt;





&lt;p&gt;Apache had stopped maintaining Log4j 1, while moving directly to Log4j 2 represented a much larger technical change.&lt;/p&gt;





&lt;p&gt;An intermediate solution was reload4j.&lt;/p&gt;





&lt;p&gt;Reload4j is a binary-compatible fork of Log4j 1.2.17 designed specifically to harden the old Log4j implementation against these kinds of vulnerabilities. Among other changes, it addressed the &lt;code&gt;JMSAppender&lt;/code&gt;, &lt;code&gt;JMSSink&lt;/code&gt;, &lt;code&gt;SocketServer&lt;/code&gt;, Chainsaw, and &lt;code&gt;JDBCAppender&lt;/code&gt; issues.&lt;/p&gt;





&lt;p&gt;Liferay adopted reload4j for earlier DXP releases while eventually moving DXP 7.4 to Log4j 2.&lt;/p&gt;





&lt;p&gt;That gave Liferay a migration path without forcing a risky architectural change into older supported releases merely to satisfy dependency scanners.&lt;/p&gt;





&lt;h2&gt;The Three Basic Outcomes&lt;/h2&gt;





&lt;p&gt;When a vulnerability is identified in a third-party dependency, there are generally three reasonable outcomes.&lt;/p&gt;





&lt;h3&gt;The Product Isn't Affected&lt;/h3&gt;





&lt;p&gt;The investigation can establish that the vulnerable capability isn't reachable through Liferay.&lt;/p&gt;





&lt;p&gt;No code change may be necessary.&lt;/p&gt;





&lt;p&gt;I tend to prefer &lt;strong&gt;not affected&lt;/strong&gt; or &lt;strong&gt;not exploitable in Liferay's usage&lt;/strong&gt; over the phrase &lt;strong&gt;false positive&lt;/strong&gt; here.&lt;/p&gt;





&lt;p&gt;The CVE itself isn't false.&lt;/p&gt;





&lt;p&gt;The scanner may have correctly identified the library.&lt;/p&gt;





&lt;p&gt;What is false is the conclusion that the CVE necessarily represents an exploitable Liferay vulnerability.&lt;/p&gt;





&lt;p&gt;This decision should never be made casually. It requires understanding both the vulnerability and Liferay's use of the affected library.&lt;/p&gt;





&lt;h3&gt;Patch or Harden the Dependency&lt;/h3&gt;





&lt;p&gt;Sometimes the vulnerability matters, but upgrading isn't practical.&lt;/p&gt;





&lt;p&gt;The upstream project may be abandoned. The fixed version may contain incompatible changes. The affected capability may be something Liferay doesn't need at all.&lt;/p&gt;





&lt;p&gt;In those cases, a focused patch or hardened dependency can make more sense than a major upgrade.&lt;/p&gt;





&lt;p&gt;Removing unused vulnerable functionality can be particularly effective because code that no longer exists has a remarkably small attack surface.&lt;/p&gt;





&lt;p&gt;Of course, maintaining patches to somebody else's software has a cost too. Whenever possible, fixes should remain aligned with or be contributed back to the upstream project rather than creating a permanent private fork.&lt;/p&gt;





&lt;h3&gt;Upgrade the Dependency&lt;/h3&gt;





&lt;p&gt;And sometimes upgrading really is the correct answer.&lt;/p&gt;





&lt;p&gt;If a compatible maintenance release contains the fix, upgrading can be relatively straightforward.&lt;/p&gt;





&lt;p&gt;The difficulty increases significantly when remediation requires a major version change.&lt;/p&gt;





&lt;p&gt;At that point this isn't a security patch anymore.&lt;/p&gt;





&lt;p&gt;It is a software upgrade.&lt;/p&gt;





&lt;h2&gt;Why Not Just Upgrade Every Time?&lt;/h2&gt;





&lt;p&gt;If you've ever upgraded Liferay itself, you already know the answer.&lt;/p&gt;





&lt;p&gt;Software libraries are dependencies because applications depend on them.&lt;/p&gt;





&lt;p&gt;Changing a major dependency can introduce:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;API incompatibilities&lt;/li&gt;
	&lt;li&gt;behavioral changes&lt;/li&gt;
	&lt;li&gt;transitive dependency changes&lt;/li&gt;
	&lt;li&gt;classloading differences&lt;/li&gt;
	&lt;li&gt;OSGi package compatibility issues&lt;/li&gt;
	&lt;li&gt;configuration changes&lt;/li&gt;
	&lt;li&gt;unexpected runtime regressions&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;Even a library that compiles successfully after an upgrade can behave differently at runtime.&lt;/p&gt;





&lt;p&gt;Then the updated platform has to be tested.&lt;/p&gt;





&lt;p&gt;Liferay releases go through extensive testing precisely because changing something deep in the platform can have consequences far away from the code that changed.&lt;/p&gt;





&lt;p&gt;This leads to a somewhat counterintuitive security principle:&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;An unnecessary security upgrade is not risk free.&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt;Suppose Liferay establishes with high confidence that a particular CVE cannot be exploited through DXP.&lt;/p&gt;





&lt;p&gt;Upgrading to a new major version of that dependency may therefore reduce essentially zero exploitable security risk while introducing new compatibility and regression risk.&lt;/p&gt;





&lt;p&gt;That isn't automatically a good trade.&lt;/p&gt;





&lt;p&gt;Security engineering should minimize &lt;strong&gt;total platform risk&lt;/strong&gt;, not maximize dependency version numbers.&lt;/p&gt;





&lt;p&gt;And engineering resources aren't infinite either.&lt;/p&gt;





&lt;p&gt;Every week spent forcing unnecessary third-party upgrades through development, compatibility work, automated testing, manual testing, stabilization, and release engineering is time not being spent fixing actual vulnerabilities or improving the product.&lt;/p&gt;





&lt;p&gt;I would much rather see those resources focused on exploitable security issues and meaningful improvements to Liferay DXP.&lt;/p&gt;





&lt;h2&gt;There Is One Important Catch: Your Code&lt;/h2&gt;





&lt;p&gt;There's another side to this discussion that customers building their own Liferay extensions need to understand.&lt;/p&gt;





&lt;p&gt;When Liferay says a third-party CVE is not exploitable in Liferay DXP, that assessment applies to &lt;strong&gt;Liferay's use of the library&lt;/strong&gt;.&lt;/p&gt;





&lt;p&gt;Your customization can change that answer.&lt;/p&gt;





&lt;p&gt;Suppose Liferay bundles a library containing a vulnerable feature but never uses that feature.&lt;/p&gt;





&lt;p&gt;Then you create an OSGi module that imports the library and starts using exactly that functionality.&lt;/p&gt;





&lt;p&gt;You may have just created the attack surface that didn't previously exist.&lt;/p&gt;





&lt;p&gt;The same principle applies if you:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;enable functionality that Liferay leaves disabled,&lt;/li&gt;
	&lt;li&gt;expose a library API through a custom REST endpoint,&lt;/li&gt;
	&lt;li&gt;pass attacker-controlled input into code that Liferay only uses with trusted data,&lt;/li&gt;
	&lt;li&gt;introduce a vulnerable appender, parser, serializer, or network listener,&lt;/li&gt;
	&lt;li&gt;or change security-related configuration around a bundled dependency.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;At that point, the original Liferay analysis may still be completely correct.&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Liferay isn't vulnerable. Your customization is.&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt;This is one reason security analysis for customized Liferay installations can't stop with Liferay's own vulnerability statements.&lt;/p&gt;





&lt;p&gt;Your code is part of the application too.&lt;/p&gt;





&lt;h2&gt;Don't Replace Liferay JARs Just to Silence a Scanner&lt;/h2&gt;





&lt;p&gt;This leads to one final warning.&lt;/p&gt;





&lt;p&gt;When customers encounter one of these findings, there can be a temptation to &amp;quot;fix&amp;quot; the problem themselves by dropping a newer version of the affected JAR into Liferay.&lt;/p&gt;





&lt;p&gt;Don't do that casually.&lt;/p&gt;





&lt;p&gt;Liferay is a large application with carefully managed dependency versions, OSGi package relationships, transitive dependencies, and runtime expectations.&lt;/p&gt;





&lt;p&gt;Replacing a platform dependency underneath it can create subtle failures that are extremely difficult to diagnose.&lt;/p&gt;





&lt;p&gt;It can also move the installation outside the configuration Liferay Support has actually tested and supports.&lt;/p&gt;





&lt;p&gt;You may successfully make the CVE disappear from the scan while introducing an entirely different production problem.&lt;/p&gt;





&lt;p&gt;That's not security improvement.&lt;/p&gt;





&lt;p&gt;That's vulnerability-report whack-a-mole.&lt;/p&gt;





&lt;h2&gt;What Should You Do With a Scanner Finding?&lt;/h2&gt;





&lt;p&gt;Security scans are still valuable.&lt;/p&gt;





&lt;p&gt;When one identifies a third-party CVE in Liferay, report it. Include the library, version, CVE, scanner output, and any information you have about how you believe the vulnerability could be exploited.&lt;/p&gt;





&lt;p&gt;But the most useful question isn't:&lt;/p&gt;





&lt;blockquote&gt;
&lt;p&gt;When will you upgrade this JAR?&lt;/p&gt;
&lt;/blockquote&gt;





&lt;p&gt;A better set of questions is:&lt;/p&gt;





&lt;blockquote&gt;
&lt;p&gt;Is Liferay DXP affected by this CVE?&lt;/p&gt;

&lt;p&gt;Is the vulnerable functionality used by Liferay?&lt;/p&gt;

&lt;p&gt;Under what configuration would the vulnerability become exploitable?&lt;/p&gt;

&lt;p&gt;Does the assessment apply only to Liferay out of the box, or are there concerns for custom OSGi modules?&lt;/p&gt;
&lt;/blockquote&gt;





&lt;p&gt;Those questions get us much closer to the thing we actually care about.&lt;/p&gt;





&lt;p&gt;Risk.&lt;/p&gt;





&lt;h2&gt;A CVE Is the Beginning of the Investigation&lt;/h2&gt;





&lt;p&gt;A CVE deserves attention.&lt;/p&gt;





&lt;p&gt;A Critical CVE deserves serious attention.&lt;/p&gt;





&lt;p&gt;But a CVE attached to a third-party JAR isn't automatically proof that the application containing that JAR is vulnerable.&lt;/p&gt;





&lt;p&gt;Good vulnerability management requires more than matching version numbers.&lt;/p&gt;





&lt;p&gt;It requires understanding attack surfaces, reachability, configuration, data flow, and how the affected software is actually being used.&lt;/p&gt;





&lt;p&gt;Sometimes that investigation should result in an immediate dependency upgrade.&lt;/p&gt;





&lt;p&gt;Sometimes it should result in a targeted patch.&lt;/p&gt;





&lt;p&gt;And sometimes the technically correct answer really is:&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Liferay DXP is not affected.&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt;That doesn't mean the CVE was ignored.&lt;/p&gt;





&lt;p&gt;Quite the opposite.&lt;/p&gt;





&lt;p&gt;It means someone did the work that the scanner couldn't.&lt;/p&gt;





&lt;p&gt;And that is exactly what we should want from a product security team.&lt;/p&gt;





&lt;p&gt;The goal isn't to make every CVE identifier disappear from a dependency report at any cost. The goal is to keep exploitable vulnerabilities out of the platform while avoiding unnecessary changes that introduce new compatibility and operational risk.&lt;/p&gt;





&lt;p&gt;So the next time a scanner finds a CVE in a third-party library bundled with Liferay, don't stop at:&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Is this version listed in the NVD?&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt;Ask the more important question:&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Can an attacker actually reach and exploit it through our Liferay environment?&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt;The scanner gives you the starting point.&lt;/p&gt;





&lt;p&gt;It doesn't always give you the verdict.&lt;/p&gt;</summary>
    <dc:creator>David H Nebinger</dc:creator>
    <dc:date>2026-08-16T19:22:00Z</dc:date>
  </entry>
  <entry>
    <title>Liferay DXP 2026.Q3 Release Webinar</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126531996" />
    <author>
      <name>Ben Turner</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126531996</id>
    <updated>2026-09-02T00:57:46Z</updated>
    <published>2026-08-11T05:32:00Z</published>
    <summary type="html">&lt;p&gt;Hi everyone,&lt;/p&gt;





&lt;p&gt;I'm super excited to announce that the Release Webinar for the 2026.Q3 release of Liferay DXP is coming in just a couple of weeks - on Wednesday, August 26th.&lt;/p&gt;





&lt;p&gt;Register now to guarantee your spot to learn about all of the new features, updated capabilities, and add-on modules.&lt;/p&gt;





&lt;p&gt;Of particular interest in Q3 are:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;AI Hub&lt;/strong&gt;, &lt;strong&gt;Content Marketing Platform&lt;/strong&gt; (CMP), &lt;strong&gt;Liferay Data Platform&lt;/strong&gt; (LDP) &lt;em&gt;and&lt;/em&gt; &lt;strong&gt;Digital Sales Rooms&lt;/strong&gt; (DSR) all move to GA status - and introduce new features&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Cloud Native&lt;/strong&gt; adds GA support for GCP Ready and Kubernetes Ready&lt;/li&gt;
	&lt;li&gt;New &lt;strong&gt;Design Libraries&lt;/strong&gt; feature brings a central place to build and reuse design assets across multiple sites.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;And, much more across the platform!&lt;/p&gt;





&lt;p&gt;Attending live lets you hear about all of the new features coming as well as letting you participate in a live Q&amp;amp;A with members of our Product Management team. &lt;/p&gt;





&lt;p&gt;While we will, as always, &lt;strong&gt;record and share&lt;/strong&gt; the session for those that can't make it, the &lt;strong&gt;Q&amp;amp;A section is only available if you attend live&lt;/strong&gt;!&lt;/p&gt;





&lt;p&gt;Click the links below to Register for the session that best suits your timezone.&lt;/p&gt;





&lt;p&gt;&lt;em&gt;(Both sessions are identical, you only need to attend one!)&lt;/em&gt;&lt;/p&gt;





&lt;table border="1" cellpadding="1"
        cellspacing="1"&gt;
	&lt;thead&gt;
		&lt;tr&gt;
			&lt;th
        scope="col"&gt;SESSION A - EMEA/APAC&lt;/th&gt;
			&lt;th scope="col"&gt;SESSION B - AMERICAS&lt;/th&gt;
		&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;Topic: 2026.Q3 Release Webinar&lt;/td&gt;
			&lt;td&gt;Topic: 2026.Q3 Release Webinar&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;Date: August 26, 2026&lt;br&gt;
			(Listed by Time Zone)&lt;/td&gt;
			&lt;td&gt;
			&lt;p&gt;Date: August 26, 2026&lt;br&gt;
			(Listed by Time Zone)&lt;/p&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;
			&lt;ul&gt;
				&lt;li&gt;08:00 - UTC&lt;/li&gt;
				&lt;li&gt;09:00 - UK/IRE (BST)&lt;/li&gt;
				&lt;li&gt;10:00 - Central Europe (CEST)&lt;/li&gt;
				&lt;li&gt;12:00 - Dubai (GST)&lt;/li&gt;
				&lt;li&gt;13:30 - India (IST)&lt;/li&gt;
				&lt;li&gt;16:00 - Singapore (SGT)&lt;/li&gt;
				&lt;li&gt;17:00 - Japan (JST)&lt;/li&gt;
				&lt;li&gt;18:00 - Sydney (AEST)&lt;/li&gt;
			&lt;/ul&gt;
			&lt;/td&gt;
			&lt;td&gt;
			&lt;ul&gt;
				&lt;li&gt;2:00 pm - Los Angeles (PDT)&lt;/li&gt;
				&lt;li&gt;3:00 pm - Mexico City (CST)&lt;/li&gt;
				&lt;li&gt;4:00 pm - Chicago (CDT)&lt;/li&gt;
				&lt;li&gt;5:00 pm - New York (EDT)&lt;/li&gt;
				&lt;li&gt;6:00 pm - Recife (BRT)&lt;/li&gt;
				&lt;li&gt;9:00 pm - UTC&lt;/li&gt;
				&lt;li&gt;7:00am - Sydney (AEDT)&lt;br&gt;
				&lt;em&gt;(August 27)&lt;/em&gt;&lt;/li&gt;
			&lt;/ul&gt;
			&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;





&lt;p&gt; &lt;/p&gt;





&lt;p&gt;To learn more, you can also check out the Release Webinar &lt;a
    href="https://www.liferay.com/web/lr/quarterly-release-webinar-q3-2026"
    rel="noopener noreferrer" target="_blank"&gt;landing page&lt;/a&gt;.&lt;/p&gt;





&lt;p&gt;See you there!&lt;/p&gt;





&lt;p&gt;Cheers,&lt;/p&gt;





&lt;p&gt;Ben&lt;/p&gt;





&lt;p&gt;======================================================&lt;/p&gt;





&lt;h4&gt;SESSION REPLAY AND PDF&lt;/h4&gt;









&lt;p&gt;So, the event is over, but the memories remain - you can now access the recording of the session along with a PDF of the presentation &lt;a
    href="https://www.liferay.com/web/lr/quarterly-release-webinar-q3-2026-recording"
    rel="noopener noreferrer" target="_blank"&gt;here&lt;/a&gt;.&lt;/p&gt;





&lt;p&gt;======================================================&lt;/p&gt;</summary>
    <dc:creator>Ben Turner</dc:creator>
    <dc:date>2026-08-11T05:32:00Z</dc:date>
  </entry>
  <entry>
    <title>OIDC SLO in Liferay DXP</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126438487" />
    <author>
      <name>David H Nebinger</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126438487</id>
    <updated>2026-08-05T17:04:04Z</updated>
    <published>2026-08-05T16:26:00Z</published>
    <summary type="html">&lt;p&gt;If you've searched for information about OpenID Connect (OIDC) Single Logout (SLO) in Liferay, you've probably come across answers stating that it isn't supported. That answer was accurate for a long time, and it still occasionally appears in support discussions today.&lt;/p&gt;





&lt;p&gt;Beginning with &lt;strong&gt;Liferay DXP 2026.Q1&lt;/strong&gt;, however, that answer is no longer correct.&lt;/p&gt;





&lt;p&gt;Liferay now supports the &lt;strong&gt;OpenID Connect Back-Channel Logout&lt;/strong&gt; specification, allowing an OpenID Provider (OP) to notify Liferay that a user's central authentication session has ended so the corresponding portal session can be terminated as well.&lt;/p&gt;





&lt;p&gt;This isn't every logout mechanism defined by the OpenID Connect specifications, and that's an important distinction. In this post we'll look at what is supported today, how it works, how to configure it, and what it means for production deployments.&lt;/p&gt;





&lt;h2&gt;Understanding the Terminology&lt;/h2&gt;





&lt;p&gt;If you're familiar with SAML, the terminology can be a little confusing because OpenID Connect uses different names for similar concepts.&lt;/p&gt;





&lt;table border="1" cellpadding="5" cellspacing="5"&gt;
	&lt;thead&gt;
		&lt;tr&gt;
			&lt;th&gt;SAML&lt;/th&gt;
			&lt;th&gt;OpenID Connect&lt;/th&gt;
		&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;Identity Provider (IdP)&lt;/td&gt;
			&lt;td&gt;OpenID Provider (OP)&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;Service Provider (SP)&lt;/td&gt;
			&lt;td&gt;Relying Party (RP)&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;





&lt;p&gt;Liferay's OpenID Connect implementation only acts as a &lt;strong&gt;Relying Party (RP)&lt;/strong&gt;.&lt;/p&gt;





&lt;p&gt;In other words, Liferay delegates authentication to an external OpenID Provider such as Keycloak, Microsoft Entra ID, Okta, or another standards-compliant identity provider. Liferay does not become an OpenID Provider itself.&lt;/p&gt;





&lt;p&gt;That distinction explains why only certain OpenID Connect specifications apply to Liferay.&lt;/p&gt;





&lt;h2&gt;There Isn't Just One &amp;quot;OIDC Logout&amp;quot;&lt;/h2&gt;





&lt;p&gt;One of the biggest sources of confusion is that people talk about &amp;quot;OIDC Single Logout&amp;quot; as though it were a single feature.&lt;/p&gt;





&lt;p&gt;It isn't.&lt;/p&gt;





&lt;p&gt;OpenID Connect defines multiple specifications for ending authenticated sessions, each designed for different scenarios.&lt;/p&gt;





&lt;p&gt;Some involve browser redirects. Others use hidden iframes. Others avoid the browser entirely and rely on secure server-to-server communication.&lt;/p&gt;





&lt;p&gt;As of &lt;strong&gt;Liferay DXP 2026.Q1&lt;/strong&gt;, Liferay implements the &lt;strong&gt;Back-Channel Logout&lt;/strong&gt; specification.&lt;/p&gt;





&lt;p&gt;Support for &lt;strong&gt;RP-Initiated Logout&lt;/strong&gt;, where Liferay requests that the OpenID Provider terminate the user's single sign-on session, is planned for a future release. As always, roadmap items are subject to change.&lt;/p&gt;





&lt;p&gt;Understanding which logout specification is being discussed is important because saying &amp;quot;OIDC logout is supported&amp;quot; or &amp;quot;OIDC logout isn't supported&amp;quot; doesn't tell the whole story.&lt;/p&gt;





&lt;h2&gt;Why Back-Channel Logout Exists&lt;/h2&gt;





&lt;p&gt;Consider a fairly typical enterprise deployment.&lt;/p&gt;





&lt;p&gt;A user authenticates through Keycloak (or any OIDC OP) and gains access to several applications.&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126438503" src="https://liferay.dev/documents/portlet_file_entry/14/slo-01.png/cdaef42b-bf15-484d-c734-1e4a1be7a224"&gt;&lt;/p&gt;





&lt;p&gt;The user now has active authenticated sessions in each application.&lt;/p&gt;





&lt;p&gt;Later, an administrator disables the account or terminates the user's identity provider session.&lt;/p&gt;





&lt;p&gt;Without Single Logout, the user has technically been logged out of the identity provider, but every application still has its own active session.&lt;/p&gt;





&lt;p&gt;The user may continue working in Liferay until the portal session expires naturally.&lt;/p&gt;





&lt;p&gt;The same may be true for Grafana, Jenkins, or any other relying party.&lt;/p&gt;





&lt;p&gt;That's clearly not ideal.&lt;/p&gt;





&lt;p&gt;Back-Channel Logout solves this problem by allowing the OpenID Provider to notify every participating application that the central authentication session has ended.&lt;/p&gt;





&lt;p&gt;Each application can then invalidate its own local session immediately.&lt;/p&gt;





&lt;h2&gt;How Back-Channel Logout Works&lt;/h2&gt;





&lt;p&gt;Unlike browser-based logout mechanisms, Back-Channel Logout is entirely server-to-server.&lt;/p&gt;





&lt;p&gt;The user's browser isn't involved.&lt;/p&gt;





&lt;p&gt;There are no redirects.&lt;/p&gt;





&lt;p&gt;There are no hidden iframes.&lt;/p&gt;





&lt;p&gt;Instead, the OpenID Provider sends a signed &lt;strong&gt;Logout Token&lt;/strong&gt; directly to each registered relying party.&lt;/p&gt;





&lt;p&gt;The overall flow looks like this.&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126438514" src="https://liferay.dev/documents/portlet_file_entry/14/slo-02.png/63b49677-e252-cb5e-d82f-c0cbff668389"&gt;&lt;/p&gt;





&lt;p&gt;From the user's perspective, nothing happens in the browser until they attempt another request.&lt;/p&gt;





&lt;p&gt;The next interaction with Liferay discovers that the authenticated session no longer exists, and the user must authenticate again.&lt;/p&gt;





&lt;h2&gt;Liferay's Implementation&lt;/h2&gt;





&lt;p&gt;Liferay's implementation follows the OpenID Foundation's Back-Channel Logout specification.&lt;/p&gt;





&lt;p&gt;Rather than implementing provider-specific integrations, Liferay focuses on standards compliance, allowing it to interoperate with any OpenID Provider that implements the same specification.&lt;/p&gt;





&lt;p&gt;Once OpenID Connect is configured, Liferay exposes the following endpoint:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;/o/open_id_connect/backchannel_logout&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;When the OpenID Provider determines that a user's authenticated session has ended, it sends a signed Logout Token to this endpoint.&lt;/p&gt;





&lt;p&gt;Liferay validates the token.&lt;/p&gt;





&lt;p&gt;If validation succeeds, Liferay identifies the corresponding authenticated portal session and invalidates it.&lt;/p&gt;





&lt;p&gt;That's the entire logout flow from Liferay's perspective.&lt;/p&gt;





&lt;h2&gt;Configuring Back-Channel Logout&lt;/h2&gt;





&lt;p&gt;The good news is that there isn't much additional configuration required inside Liferay.&lt;/p&gt;





&lt;p&gt;If OpenID Connect is already configured, enabling Back-Channel Logout primarily involves registering Liferay's logout endpoint with your OpenID Provider.&lt;/p&gt;





&lt;p&gt;The process typically looks like this:&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Configure OpenID Connect normally in Liferay.&lt;/li&gt;
	&lt;li&gt;Configure your OpenID Provider as you normally would.&lt;/li&gt;
	&lt;li&gt;Register Liferay's Back-Channel Logout endpoint:&lt;/li&gt;
&lt;/ol&gt;





&lt;pre&gt;
&lt;code&gt;https://your-liferay-host/o/open_id_connect/backchannel_logout&lt;/code&gt;&lt;/pre&gt;





&lt;ol&gt;
	&lt;li&gt;Enable Back-Channel Logout support in your identity provider, if required.&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;The exact steps for registering the endpoint vary by provider, but the concept remains the same regardless of whether you're using Keycloak, Microsoft Entra ID, Okta, or another standards-compliant implementation.&lt;/p&gt;





&lt;h2&gt;Security Considerations&lt;/h2&gt;





&lt;p&gt;At first glance, exposing a logout endpoint may seem concerning.&lt;/p&gt;





&lt;p&gt;What prevents someone from sending arbitrary HTTP POST requests and logging users out?&lt;/p&gt;





&lt;p&gt;The answer is the Logout Token.&lt;/p&gt;





&lt;p&gt;The OpenID Provider sends a cryptographically signed Logout Token that identifies the authenticated session being terminated.&lt;/p&gt;





&lt;p&gt;Liferay validates that token before taking any action.&lt;/p&gt;





&lt;p&gt;If validation fails, the logout request is rejected.&lt;/p&gt;





&lt;p&gt;In other words, the endpoint isn't a public &amp;quot;log out any user&amp;quot; API.&lt;/p&gt;





&lt;p&gt;It only accepts properly signed logout notifications from the configured OpenID Provider.&lt;/p&gt;





&lt;h2&gt;Operational Considerations&lt;/h2&gt;





&lt;p&gt;Although the implementation itself is straightforward, there are a few operational details worth remembering.&lt;/p&gt;





&lt;h3&gt;The OpenID Provider Must Reach Liferay&lt;/h3&gt;





&lt;p&gt;Back-Channel Logout is initiated by the OpenID Provider, not the user's browser.&lt;/p&gt;





&lt;p&gt;That means your identity provider must be able to reach Liferay's Back-Channel Logout endpoint.&lt;/p&gt;





&lt;p&gt;In practice, this means paying attention to reverse proxy configuration, firewall rules, DNS resolution, TLS certificates, and public endpoint accessibility.&lt;/p&gt;





&lt;p&gt;If the OpenID Provider cannot successfully deliver the Logout Token, Liferay has no way of knowing that the central authentication session has ended.&lt;/p&gt;





&lt;h3&gt;Browser Behavior&lt;/h3&gt;





&lt;p&gt;Because this is a server-to-server protocol, users won't necessarily notice anything immediately.&lt;/p&gt;





&lt;p&gt;If they're actively viewing a page inside Liferay, nothing visibly changes.&lt;/p&gt;





&lt;p&gt;The logout is detected the next time the browser communicates with the portal.&lt;/p&gt;





&lt;p&gt;This behavior is expected and is one of the defining characteristics of the Back-Channel Logout specification.&lt;/p&gt;





&lt;h3&gt;Clustered Liferay Environments&lt;/h3&gt;





&lt;p&gt;Back-Channel Logout does not introduce a special clustering requirement.&lt;/p&gt;





&lt;p&gt;If a Liferay environment is using OpenID Connect Single Sign-On, each node already participates in the same authentication model. Once the OpenID Provider ends the session and Liferay processes the Logout Token, subsequent attempts to use that authenticated session should fail regardless of which cluster node receives the request.&lt;/p&gt;





&lt;p&gt;The important operational concern is therefore not session replication between Liferay nodes. It is ensuring that the OpenID Provider can reach Liferay's Back-Channel Logout endpoint and that the Logout Token can be validated successfully.&lt;/p&gt;





&lt;h2&gt;What Isn't Supported Yet?&lt;/h2&gt;





&lt;p&gt;Another OpenID Connect specification is &lt;strong&gt;RP-Initiated Logout&lt;/strong&gt;.&lt;/p&gt;





&lt;p&gt;Instead of the identity provider initiating logout, the relying party starts the process.&lt;/p&gt;





&lt;p&gt;For example, imagine a user clicking &lt;strong&gt;Log Out&lt;/strong&gt; inside Liferay.&lt;/p&gt;





&lt;p&gt;Rather than only invalidating the local portal session, Liferay could also request that the OpenID Provider terminate the user's central single sign-on session.&lt;/p&gt;





&lt;p&gt;That, in turn, would allow the identity provider to notify every participating application.&lt;/p&gt;





&lt;p&gt;The flow would look something like this.&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126438525" src="https://liferay.dev/documents/portlet_file_entry/14/slo-03.png/a6f78979-d3de-5275-91fc-8cf6da657ca9"&gt;&lt;/p&gt;





&lt;p&gt;Support for RP-Initiated Logout is planned for a future Liferay release, although, as with any roadmap item, plans may change.&lt;/p&gt;





&lt;p&gt;When both specifications are available, they complement one another rather than replace one another.&lt;/p&gt;





&lt;p&gt;Back-Channel Logout allows the OpenID Provider to terminate application sessions.&lt;/p&gt;





&lt;p&gt;RP-Initiated Logout allows an application to terminate the user's central identity provider session.&lt;/p&gt;





&lt;p&gt;Together they provide a more complete single logout experience.&lt;/p&gt;





&lt;h2&gt;Final Thoughts&lt;/h2&gt;





&lt;p&gt;The important takeaway is that saying &lt;strong&gt;&amp;quot;Liferay doesn't support OIDC Single Logout&amp;quot;&lt;/strong&gt; is no longer accurate.&lt;/p&gt;





&lt;p&gt;Beginning with &lt;strong&gt;Liferay DXP 2026.Q1&lt;/strong&gt;, Liferay supports the OpenID Connect &lt;strong&gt;Back-Channel Logout&lt;/strong&gt; specification, allowing standards-compliant OpenID Providers to securely terminate authenticated portal sessions without relying on browser redirects or provider-specific integrations.&lt;/p&gt;





&lt;p&gt;If your organization already uses OpenID Connect for Single Sign-On, enabling Back-Channel Logout helps ensure that when the user's central authentication session ends, their Liferay session ends as well. That's an important improvement for both security and operational consistency across multiple applications.&lt;/p&gt;





&lt;p&gt;Support for RP-Initiated Logout would make the logout story even more complete, but the existing Back-Channel Logout implementation already addresses one of the most important challenges in federated authentication: keeping application sessions synchronized with the identity provider.&lt;/p&gt;





&lt;p&gt;As more organizations adopt centralized identity platforms, standards-based logout becomes just as important as standards-based login. It's good to see Liferay continuing to build on the OpenID Connect specifications in a way that improves interoperability while remaining provider agnostic.&lt;/p&gt;





&lt;p&gt;For more information, check out &lt;a
    href="https://learn.liferay.com/w/dxp/security-and-administration/security/configuring-sso/using-openid-connect#technical-standards-and-specifications" rel="noopener noreferrer"&gt;Liferay's documentation for OIDC&lt;/a&gt;.&lt;/p&gt;</summary>
    <dc:creator>David H Nebinger</dc:creator>
    <dc:date>2026-08-05T16:26:00Z</dc:date>
  </entry>
  <entry>
    <title>Bringing Liferay to Cross-platform Development</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126437387" />
    <author>
      <name>Nestor Ledon</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126437387</id>
    <updated>2026-08-05T14:11:00Z</updated>
    <published>2026-08-05T13:37:00Z</published>
    <summary type="html">&lt;p&gt;Liferay already provides a mature platform for content, permissions, users, sites, workflows, and headless APIs. What has been missing is a practical way to use that platform as the backend for a modern application.&lt;/p&gt;





&lt;p&gt;Liferay previously offered dedicated mobile libraries, but those tools were ultimately deprecated or removed in favor of Headless Delivery. That was probably the right architectural move, even if retiring the mobile SDK without replacing the developer experience left a pretty obvious gap. API-first competitors such as Contentful have already shown why separating the backend from the presentation layer makes sense.&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;KDXP&lt;/strong&gt; is intended to fill that gap: not by bringing back another platform-specific mobile SDK, but by providing a Kotlin &lt;strong&gt;Multiplatform framework&lt;/strong&gt; over Liferay’s headless APIs for Android, iOS, desktop, and web.&lt;/p&gt;





&lt;p&gt;The gap is obvious: searching YouTube for &lt;em&gt;&lt;strong&gt;“Liferay Kotlin”&lt;/strong&gt;&lt;/em&gt; currently returns the KDXP setup demo on the first page. Searching for &lt;strong&gt;&lt;em&gt;“Liferay multi-platform”&lt;/em&gt;&lt;/strong&gt; does the same.&lt;/p&gt;





&lt;p&gt;Those results do not mean KDXP is finished. They do suggest that developers looking to combine Liferay with modern cross-platform development currently have very few options.&lt;/p&gt;





&lt;p&gt;The first setup demo walks through connecting KDXP to a local Liferay installation, configuring OAuth, and creating an application-backed site.&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Watch the KDXP Liferay Setup demo:&lt;/strong&gt;&lt;br&gt;
&lt;a
    href="https://youtu.be/mlPIv3XQ-GY?si=aSFYDsVCN0vhPTLd" rel="noopener noreferrer"&gt;https://youtu.be/mlPIv3XQ-GY?si=aSFYDsVCN0vhPTLd&lt;/a&gt;&lt;/p&gt;





&lt;p&gt;This is the first step toward giving Liferay developers a shared application layer across Android, iOS, desktop, and web.&lt;/p&gt;





&lt;p&gt;&lt;em&gt;YouTube results captured August 4, 2026.&lt;/em&gt;&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Connect with me on LinkedIn:&lt;/strong&gt;&lt;br&gt;
&lt;a
    data-end="1236" data-start="1196"
    href="https://www.linkedin.com/in/nestorledon/" rel="noopener" target="_new"&gt;https://www.linkedin.com/in/nestorledon/&lt;/a&gt;&lt;/p&gt;</summary>
    <dc:creator>Nestor Ledon</dc:creator>
    <dc:date>2026-08-05T13:37:00Z</dc:date>
  </entry>
  <entry>
    <title>No More Rebuild-and-Redeploy: Instant Hot Reload for Liferay React Client Extensions</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126396924" />
    <author>
      <name>Ankit Hadiyal</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126396924</id>
    <updated>2026-08-02T23:42:05Z</updated>
    <published>2026-08-01T10:10:00Z</published>
    <summary type="html">&lt;h2&gt;The Rebuild-and-Redeploy Loop Every Client Extension Developer Knows&lt;/h2&gt;





&lt;p data-sourcepos="10:1-10:96;370-465"&gt;If you've built a React-based client extension (CX) for Liferay DXP, you know this rhythm well.&lt;/p&gt;





&lt;p data-sourcepos="12:1-12:464;467-930"&gt;You tweak a component - adjust some padding, change a label, wire up a new prop. You save the file, switch to your terminal, and kick off a build. Gradle picks it up, packages it into a client extension bundle, and pushes it to your running Liferay instance. Twenty or thirty seconds later, sometimes more depending on your machine and workspace size, it's finally deployed. You switch back to the browser, hard-refresh, and check whether the padding looks right.&lt;/p&gt;





&lt;p data-sourcepos="14:1-14:31;932-962"&gt;It didn't. So you do it again.&lt;/p&gt;





&lt;p data-sourcepos="16:1-16:197;964-1160"&gt;Multiply that by the dozens of small adjustments that go into any real UI, and the wasted time adds up fast. An afternoon of front-end polish can turn into an afternoon of watching a progress bar.&lt;/p&gt;





&lt;p data-sourcepos="18:1-18:494;1162-1655"&gt;This isn't really a Liferay problem - it's what frontend development looked like everywhere before tools like Vite made Hot Module Replacement (HMR) the default. Most React developers left that world behind years ago. Client extension developers are still living in it by default, because a client extension isn't a standalone web app you serve yourself. It's a bundle Liferay loads from a URL declared in &lt;code&gt;client-extension.yaml&lt;/code&gt;, and that bundle only changes when you rebuild and redeploy it.&lt;/p&gt;





&lt;h2&gt;Liferay Already Gives You the Fix — Here's Where It Stops&lt;/h2&gt;





&lt;p data-sourcepos="22:1-22:386;1724-2109"&gt;To be fair, this isn't an unsolved problem. Liferay's own frontend team has documented a way to skip the redeploy step, using a file called &lt;code&gt;client-extension.dev.yaml&lt;/code&gt;. Instead of pointing your custom element's entry at a bundled production file, you point it at your local Vite dev -  server  &lt;code&gt;http://localhost:5173&lt;/code&gt;, say , so Liferay loads the source straight from Vite while you work.&lt;/p&gt;





&lt;p data-sourcepos="24:1-24:167;2111-2277"&gt;That alone is worth doing: no more waiting on Gradle for every change. But if you've tried it, you've probably noticed it doesn't fully deliver on the promise of HMR.&lt;/p&gt;





&lt;p data-sourcepos="26:1-26:320;2279-2598"&gt;For plain JS or CSS, it behaves close to what you'd expect. For a React custom element, saving a file still triggers a full page reload instead of an in-place swap of just the component you changed. You lose whatever state you were looking at, and you don't get the sub-second feedback loop Vite is known for elsewhere.&lt;/p&gt;





&lt;p data-sourcepos="28:1-28:388;2600-2987"&gt;Getting genuine component-level HMR — the kind where only the changed component re-renders and state survives means adding Vite's client script and a small React Refresh runtime shim as their own dev-only entries, loaded in a specific order, on top of the dev.yaml file. It works, but it's fiddly, and it's boilerplate you end up rewriting for every client extension in your workspace.&lt;/p&gt;





&lt;p data-sourcepos="30:1-30:83;2989-3071"&gt;Two other things tend to bite people once a project grows past a single extension:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Port collisions.&lt;/strong&gt; Every client extension wants its own Vite dev server. With five, ten, or twenty of them in the same workspace not unusual on a larger DXP project - you end up manually tracking which port belongs to which project so they don't collide.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Imports that only make sense at runtime.&lt;/strong&gt; Some packages your custom element imports things like &lt;code&gt;@liferay/oauth2-provider-web/client&lt;/code&gt;, or shared &lt;code&gt;@clayui&lt;/code&gt; components — aren't meant to live in your local &lt;code&gt;node_modules&lt;/code&gt;. In production, Liferay serves them once from a shared registry so every client extension on the page reuses the same instance instead of shipping its own copy. Vite's dev server doesn't know that convention exists. The moment your code imports one of those packages, Vite either can't resolve it or bundles a second, disconnected copy of something that's supposed to be a singleton.&lt;/li&gt;
&lt;/ul&gt;





&lt;p data-sourcepos="35:1-35:226;3944-4169"&gt;None of this is exotic. It's documented, and any experienced CX developer can wire it up by hand. The problem is that &amp;quot;by hand&amp;quot; means redoing a multi-step setup, correctly, for every extension, on every project, indefinitely.&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;
	&lt;h2&gt;Introducing &lt;code&gt;liferay-cx-hmr-setup&lt;/code&gt;&lt;/h2&gt;
	&lt;/li&gt;
&lt;/ul&gt;





&lt;p data-sourcepos="39:1-39:307;4210-4516"&gt;We built &lt;code&gt;liferay-cx-hmr-setup&lt;/code&gt;, an npm CLI tool, to take care of all of that in one pass. Run it once against your Liferay workspace, and it configures instant, component-level HMR for every React client extension it finds — no hand-written YAML, no manually authored refresh scripts, no port spreadsheet.&lt;/p&gt;





&lt;p&gt;bash&lt;/p&gt;





&lt;div class="overflow-auto portlet-msg-info"&gt;npx liferay-cx-hmr-setup&lt;/div&gt;





&lt;p data-sourcepos="45:1-45:186;4556-4741"&gt;From that point on, your regular &lt;code&gt;npm run dev&lt;/code&gt; behaves the way you'd expect from any modern React project: save a file, see the change on the actual Liferay page in well under a second.&lt;/p&gt;





&lt;h2&gt;How It Works, In Plain Words&lt;/h2&gt;





&lt;p data-sourcepos="49:1-49:42;4776-4817"&gt;Here's what happens when you run the CLI:&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;It scans your Liferay workspace and finds every client extension built with Vite.&lt;/li&gt;
	&lt;li&gt;For each one, it checks which local ports are already claimed by neighboring extensions and assigns the next free one.&lt;/li&gt;
	&lt;li&gt;It generates or updates that extension's &lt;code&gt;client-extension.dev.yaml&lt;/code&gt;, pointing its entries at &lt;code&gt;localhost&lt;/code&gt; on the assigned port.&lt;/li&gt;
	&lt;li&gt;It adds a small dev-only preamble script and registers it to load ahead of your main entry point, so Vite's client and the React Refresh runtime are ready before your component code runs.&lt;/li&gt;
	&lt;li&gt;It updates &lt;code&gt;vite.config&lt;/code&gt; for that project: turning on CORS so the portal's origin can load it, and registering a plugin that recognizes Liferay-provided imports and leaves them for the browser to resolve at runtime instead of bundling them.&lt;/li&gt;
&lt;/ol&gt;





&lt;p data-sourcepos="57:1-57:241;5593-5833"&gt;None of this is a new mechanism — it's the same dev.yaml and Vite dev server approach Liferay already supports. The tool just handles the repetitive, error-prone parts of setting it up so you're not redoing them by hand for every extension.&lt;/p&gt;





&lt;h2&gt;What Gets Set Up Automatically&lt;/h2&gt;





&lt;h3&gt;Automatic Port Allocation Across Your Whole Workspace&lt;/h3&gt;





&lt;p data-sourcepos="63:1-63:265;5929-6193"&gt;On a workspace with a couple of client extensions, hand-assigning ports isn't a big deal. On one with fifteen or twenty, it becomes its own small chore, and it's easy to introduce a collision without noticing until two dev servers refuse to start at the same time.&lt;/p&gt;





&lt;p data-sourcepos="65:1-65:435;6195-6629"&gt;The CLI handles this by checking every neighboring client extension project, reading which ports are already taken, and picking the next open one: &lt;code&gt;5173&lt;/code&gt;, &lt;code&gt;5174&lt;/code&gt;, &lt;code&gt;5175&lt;/code&gt;, and so on. That port gets written into both the extension's Vite config and its &lt;code&gt;client-extension.dev.yaml&lt;/code&gt;, so the two never drift out of sync. Scaffold a new client extension later, run the CLI again, and it slots in without disturbing anything already running.&lt;/p&gt;





&lt;h3&gt;Letting Liferay's Runtime Packages Do Their Job&lt;/h3&gt;





&lt;p data-sourcepos="69:1-69:266;6684-6949"&gt;Rather than making you manually mark specific imports as external in &lt;code&gt;vite.config&lt;/code&gt;, the tool ships a small Vite plugin that recognizes packages Liferay provides at runtime and leaves those import statements alone instead of trying to resolve or bundle them locally.&lt;/p&gt;





&lt;p data-sourcepos="71:1-71:416;6951-7366"&gt;That matters because these packages are either missing from your local &lt;code&gt;node_modules&lt;/code&gt; entirely, or present only as a type-checking dependency, not something meant to be duplicated inside your bundle. With the plugin in place, your dev environment resolves those imports the same way production does, against whatever Liferay has published for the page, instead of a second, disconnected copy Vite tried to guess at.&lt;/p&gt;





&lt;h3&gt;Zero-Config Wiring: dev.yaml, the Refresh Preamble, and CORS&lt;/h3&gt;





&lt;p data-sourcepos="75:1-75:541;7434-7974"&gt;This is the part that used to take the most trial and error. The CLI generates &lt;code&gt;client-extension.dev.yaml&lt;/code&gt; for the extension, along with the small refresh script it points to, and makes sure the refresh entry loads ahead of the client entry — order matters here, and it's an easy detail to get backwards by hand. It also turns on the CORS headers your Vite dev server needs so the browser doesn't block requests coming from the portal's origin, and links the externals plugin from the previous section into your &lt;code&gt;vite.config&lt;/code&gt; automatically.&lt;/p&gt;





&lt;p data-sourcepos="77:1-77:197;7976-8172"&gt;Run the CLI again after scaffolding a new client extension, and all of this gets set up for the new project too. You're not maintaining a growing pile of hand-edited config as the workspace grows.&lt;/p&gt;





&lt;h2&gt;Getting Started&lt;/h2&gt;





&lt;p data-sourcepos="81:1-81:220;8194-8413"&gt;You'll need a React client extension project that already uses Vite as its bundler (the standard approach since Create React App was deprecated), Node and npm installed, and a Liferay DXP instance you can reach locally.&lt;/p&gt;





&lt;h3&gt;1. Run the CLI&lt;/h3&gt;





&lt;p&gt;bash&lt;/p&gt;





&lt;div class="overflow-auto portlet-msg-info"&gt;npx liferay-cx-hmr-setup&lt;/div&gt;





&lt;p data-sourcepos="89:1-89:105;8473-8577"&gt;This scans your workspace and configures every Vite-based client extension it finds, as described above.&lt;/p&gt;





&lt;h3&gt;2. Deploy the Dev Manifest&lt;/h3&gt;





&lt;p&gt;bash&lt;/p&gt;





&lt;div class="overflow-auto portlet-msg-info"&gt;./gradlew :client-extensions:my-cx:deployDev&lt;/div&gt;





&lt;p data-sourcepos="97:1-97:250;8669-8918"&gt;This tells Liferay, for this extension, to load whatever's being served from your local Vite dev server instead of the last built bundle. You only need to re-run this if the extension's entries or assigned port change, not for ordinary code changes.&lt;/p&gt;





&lt;h3&gt;3. Start Coding&lt;/h3&gt;





&lt;p&gt;bash&lt;/p&gt;





&lt;div class="overflow-auto portlet-msg-info"&gt;npm run dev&lt;/div&gt;





&lt;p data-sourcepos="105:1-105:252;8966-9217"&gt;Vite boots on the port assigned to that extension. Open the page where the extension is placed, keep it next to your editor, and change something small — a color, a string of text. It updates before you've moved your cursor back to the browser window.&lt;/p&gt;





&lt;p data-sourcepos="107:1-107:143;9219-9361"&gt;Component edits swap in place with state preserved wherever React Refresh can manage it. CSS updates apply immediately, with no reload at all.&lt;/p&gt;





&lt;h2&gt;What This Changes About Your Day-to-Day&lt;/h2&gt;





&lt;p data-sourcepos="111:1-111:340;9407-9746"&gt;The time savings compound quickly on any project with real UI work. Instead of a 20–30 second round trip for every change, you get the sub-second feedback loop of a standalone React app, which matters most exactly when you need it most: while you're iterating on layout, spacing, and visual details that take several attempts to get right.&lt;/p&gt;





&lt;p data-sourcepos="113:1-113:211;9748-9958"&gt;It also removes friction when onboarding someone new onto a CX-heavy codebase. Instead of walking them through hand-writing dev YAML and refresh scripts for their first extension, you point them at one command.&lt;/p&gt;





&lt;p data-sourcepos="115:1-115:337;9960-10296"&gt;Importantly, none of this touches your production path. &lt;code&gt;client-extension.dev.yaml&lt;/code&gt;, the preamble script, and your Vite dev config are all dev-only additions. Your actual &lt;code&gt;client-extension.yaml&lt;/code&gt; and the Gradle build your CI/CD runs stay exactly as they were. Turning this on for local development carries no risk to what actually ships.&lt;/p&gt;





&lt;h2&gt;Give It a Try&lt;/h2&gt;





&lt;p data-sourcepos="119:1-119:277;10316-10592"&gt;&lt;code&gt;liferay-cx-hmr-setup&lt;/code&gt; is a new tool, available now on npm, built to solve the exact friction we kept running into on our own client extension-heavy projects. If you try it and hit a rough edge, or think of a feature that would make it more useful, we'd like to hear about it.&lt;/p&gt;





&lt;p data-sourcepos="121:1-121:119;10594-10712"&gt;Every report and suggestion helps make this more useful for the next team building React client extensions on Liferay.&lt;/p&gt;</summary>
    <dc:creator>Ankit Hadiyal</dc:creator>
    <dc:date>2026-08-01T10:10:00Z</dc:date>
  </entry>
  <entry>
    <title>Service Builder Compatibility Matrix</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126389560" />
    <author>
      <name>David H Nebinger</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126389560</id>
    <updated>2026-07-31T18:51:50Z</updated>
    <published>2026-07-30T17:54:00Z</published>
    <summary type="html">&lt;h2&gt;Why This Matters&lt;/h2&gt;





&lt;p&gt;When you update Blade CLI and create a new Liferay Workspace, the generated workspace normally uses the latest Liferay Workspace Gradle plugin. That plugin selects versions for a number of supporting build tools, including the Service Builder plugin.&lt;/p&gt;





&lt;p&gt;For new projects targeting the latest Liferay release, this is usually what you want.&lt;/p&gt;





&lt;p&gt;The detail worth paying attention to is that Service Builder generates Java code against the service APIs and conventions of a specific Liferay release line. As Liferay evolves from one quarterly release to another, such as from &lt;code&gt;2026.Q1&lt;/code&gt; to &lt;code&gt;2026.Q2&lt;/code&gt;, the service tier can change.&lt;/p&gt;





&lt;p&gt;The latest Service Builder plugin knows how to generate code for the latest platform version, but that does not always mean it is the right choice for an older target platform.&lt;/p&gt;





&lt;p&gt;For example, if your workspace is targeting &lt;code&gt;2024.Q1&lt;/code&gt;, using the newest Service Builder plugin may generate service code that does not line up with the APIs available in that Liferay version.&lt;/p&gt;





&lt;p&gt;The important point is simple:&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Your Service Builder plugin version should match the Liferay DXP version you are targeting in your workspace.&lt;/strong&gt;&lt;/p&gt;





&lt;h2&gt;The Compatibility Matrix&lt;/h2&gt;





&lt;p&gt;The Developer Experience team at Liferay has put together the following compatibility chart. Use it to select the Service Builder plugin version that corresponds to the Liferay quarterly release configured in your workspace.&lt;/p&gt;





&lt;table border="1" cellpadding="5" cellspacing="5"&gt;
	&lt;thead&gt;
		&lt;tr&gt;
			&lt;th&gt;Liferay Quarterly Version&lt;/th&gt;
			&lt;th&gt;Service Builder Version&lt;/th&gt;
		&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;2023.Q3&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.463&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;2023.Q4&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.470&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;2024.Q1&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.478&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;2024.Q2&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.485&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;2024.Q3&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.488&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;2024.Q4&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.490&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;2025.Q1&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.496&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;2025.Q2&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.503&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;2025.Q3&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.510&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;2025.Q4&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.513&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;2026.Q1&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.514&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;2026.Q2&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.532&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;





&lt;p&gt;The Service Builder version listed for a quarterly release applies to the patch releases within that quarterly release line.&lt;/p&gt;





&lt;h2&gt;How To Choose The Right Version&lt;/h2&gt;





&lt;p&gt;Start with the Liferay version your workspace is targeting.&lt;/p&gt;





&lt;p&gt;In a Liferay Workspace, this is typically configured in &lt;code&gt;gradle.properties&lt;/code&gt; using the &lt;code&gt;liferay.workspace.product&lt;/code&gt; property:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;liferay.workspace.product=dxp-2024.q1.10-lts&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;The quarterly release portion of that value is the part you need to match against the table.&lt;/p&gt;





&lt;table&gt;
	&lt;thead&gt;
		&lt;tr&gt;
			&lt;th&gt;Workspace Target Platform&lt;/th&gt;
			&lt;th&gt;Quarterly Release&lt;/th&gt;
			&lt;th&gt;Service Builder Version&lt;/th&gt;
		&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;dxp-2024.q1.10-lts&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;2024.Q1&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.478&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;dxp-2026.q1.8-lts&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;2026.Q1&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.514&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;&lt;code&gt;dxp-2026.q2.0&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;2026.Q2&lt;/code&gt;&lt;/td&gt;
			&lt;td&gt;&lt;code&gt;1.0.532&lt;/code&gt;&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;





&lt;p&gt;Once you know the matching Service Builder version, pin it in the same &lt;code&gt;gradle.properties&lt;/code&gt; file:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;com.liferay.portal.tools.service.builder.version=1.0.478&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;So a workspace targeting &lt;code&gt;2024.Q1&lt;/code&gt; might include:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;liferay.workspace.product=dxp-2024.q1.10-lts
com.liferay.portal.tools.service.builder.version=1.0.478&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;After changing the property, rebuild your Service Builder modules as usual:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;blade gw buildService
blade gw jar&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Or, if you prefer compiling directly:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;blade gw buildService
blade gw compileJava&lt;/code&gt;&lt;/pre&gt;





&lt;div class="overflow-auto portlet-msg-alert"&gt;&lt;strong&gt;NOTE: &lt;/strong&gt;If you have previously generated the service classes but didn't specify the right version of the SB plugin, you should clear out all of the generated code. The &lt;code&gt;buildService&lt;/code&gt; task does not see a change in plugin version as a reason to regenerate a class, only a change to &lt;code&gt;service.xml&lt;/code&gt; or. one of the impl classes will do that, and only targeted to those classes that need to be updated as a result of the change. Purging all of the previously generated classes will ensure that SB generates all of the right classes for your version of Liferay DXP.&lt;/div&gt;





&lt;h2&gt;When To Recheck The Matrix&lt;/h2&gt;





&lt;p&gt;You should recheck this matrix whenever you change your workspace target platform.&lt;/p&gt;





&lt;p&gt;For example, if you move from:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;liferay.workspace.product=dxp-2024.q1.10-lts&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;to:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;liferay.workspace.product=dxp-2026.q2.0&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;then you should also update:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;com.liferay.portal.tools.service.builder.version=1.0.532&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;The target platform and Service Builder version should move together. Treat them as related build configuration, not as independent settings.&lt;/p&gt;





&lt;h2&gt;Practical Workflow&lt;/h2&gt;





&lt;ol&gt;
	&lt;li&gt;Set or update &lt;code&gt;liferay.workspace.product&lt;/code&gt;.&lt;/li&gt;
	&lt;li&gt;Find the matching quarterly release in the compatibility matrix.&lt;/li&gt;
	&lt;li&gt;Pin &lt;code&gt;com.liferay.portal.tools.service.builder.version&lt;/code&gt; to the matching version.&lt;/li&gt;
	&lt;li&gt;Run &lt;code&gt;buildService&lt;/code&gt;.&lt;/li&gt;
	&lt;li&gt;Compile the module.&lt;/li&gt;
	&lt;li&gt;Commit both the generated service code and the workspace configuration change together.&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;That last point matters. If generated Service Builder code changes because the plugin version changed, future maintainers need to see the configuration change that explains why the generated code changed.&lt;/p&gt;





&lt;h2&gt;Looking Ahead&lt;/h2&gt;





&lt;p&gt;The Liferay Dev Tools team is working on ways to make this selection automatic in the future. A future version of Blade CLI, the Liferay Workspace plugin, or both may be able to select the appropriate Service Builder version based on the configured target platform.&lt;/p&gt;





&lt;p&gt;Until then, this matrix gives you a practical way to avoid generating service code that does not align with your Liferay DXP version.&lt;/p&gt;</summary>
    <dc:creator>David H Nebinger</dc:creator>
    <dc:date>2026-07-30T17:54:00Z</dc:date>
  </entry>
  <entry>
    <title>Batch Imports Audit</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126385336" />
    <author>
      <name>Vitalii Koshelenko</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126385336</id>
    <updated>2026-07-29T12:05:29Z</updated>
    <published>2026-07-29T09:07:00Z</published>
    <summary type="html">&lt;h2&gt;Introduction&lt;/h2&gt;





&lt;p&gt;Liferay's &lt;a href="https://learn.liferay.com/w/dxp/integration/headless-apis/using-liferay-as-a-headless-platform/consuming-apis/batch-engine-api-basics-importing-data"&gt;Batch Engine&lt;/a&gt; is the workhorse behind bulk data movement into Liferay. Instead of creating records one REST call at a time, you POST (or PUT, or DELETE) an array of items to a /batch endpoint for the entity you care about — Objects, Commerce catalogs, channels, product specifications, whatever — and the Batch Engine takes it from there. It queues the work as an asynchronous import task, processes the records, and updates that task's status as it goes, from INITIAL through to COMPLETED (or FAILED).&lt;/p&gt;





&lt;p&gt;That async, fire-and-forget model is exactly what you want when an external system needs to push thousands of records into Liferay without waiting around for a synchronous response. It's also exactly what makes the next question so important: once you've fired that batch off, how do you know &lt;strong&gt;what actually happened to it?&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt; &lt;/p&gt;





&lt;h2&gt;Problem Overview&lt;/h2&gt;





&lt;p&gt;This came up for real on a large-scale B2B commerce project we've been building. The setup is a common one:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;
	&lt;p&gt;Product data — categories, specifications, products, pricing, you name it — lives in an external PIM system, not in Liferay.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p&gt;An integration middleware layer owns the sync: it reads from the PIM and calls Liferay's Headless Batch APIs to push data in.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p&gt;Liferay is the consumer, not the source of truth. From Liferay's side, batches just show up.&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;And, inevitably, some of those batches fail. The reasons vary:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;
	&lt;p&gt;Invalid data — a field that doesn't match the expected format or type;&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p&gt;Duplicate data — an external reference code or any other field that's already in use;&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p&gt;Internal Batch Engine errors — timeouts, constraint violations, anything else that can go wrong mid-import.&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;Here's the problem: Liferay doesn't ship a UI for any of this. When the middleware team says &amp;quot;we pushed 500 products and something failed,&amp;quot; the only way to find out what, exactly, is to go spelunking through application logs or query the database directly — and that's assuming you have that kind of access to a client's environment in the first place. &lt;/p&gt;





&lt;p&gt;But Liferay already has the answer. Every batch import task the Batch Engine runs is persisted — its status, timing, item counts, and errors — in its own tables (&lt;code&gt;BatchEngineImportTask&lt;/code&gt; and &lt;code&gt;BatchEngineImportTaskError&lt;/code&gt;). The data we needed was sitting right there. It just wasn't visible to anyone.&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126385344"
    src="https://liferay.dev/documents/portlet_file_entry/14/00-db.png/74e74559-6f19-6f2e-69ce-03416d751299"
    style="height: auto;width: 352.0px;display: block;margin-left: auto;margin-right: auto;" width="352"&gt;&lt;/p&gt;





&lt;p&gt; &lt;/p&gt;





&lt;h2&gt;Solution Overview&lt;/h2&gt;





&lt;p&gt;So we built Batch Import Audit: a Control Panel tool that puts the Batch Engine's own record-keeping in front of the people who actually need it — support engineers, integration developers, and admins — without anyone touching a database client.&lt;/p&gt;





&lt;p&gt;The tool adds a new Monitoring category to the Control Panel, with a Batch Import Audit app underneath it. Conceptually, the relationship to the underlying data is simple:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;
	&lt;p&gt;BatchEngineImportTask the source for the main list: one row per import task, with its class name, execute status, initiating user, start/end time, and total vs. processed item counts.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p&gt;BatchEngineImportTaskError is the source for what went wrong: when a task fails, this is where the record-level detail lives — which item failed, and why.&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;The list view gives you every import task the Batch Engine has ever run, newest first, with filters for:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;
	&lt;p&gt;Initiator — which user (or integration account) triggered the import&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p&gt;Execute Status — e.g. completed, failed, in progress&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p&gt;Class Name — which entity type was being imported (catalogs, channels, products, etc.)&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p&gt;External Reference Code — find the task that touched a specific record, by its ERC&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;&lt;img data-fileentryid="126385363" src="https://liferay.dev/documents/portlet_file_entry/14/02-filter.png/ab512dde-24d1-4197-d5b6-688102013ea5"&gt;&lt;/p&gt;





&lt;p&gt;The details view is where the actual troubleshooting happens. Open any task and you get its full metadata — batch size, content type, import strategy, operation, timing — along with the original request payload, unpacked and pretty-printed exactly as it was submitted. For failed tasks, this is also where the task-level error information and the underlying error records surface, so you can see not just that something failed, but which record and why, without ever leaving the browser.&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126385379" src="https://liferay.dev/documents/portlet_file_entry/14/03-details.png/e856a4e6-ff0c-9b87-eca1-baa50624cc76"&gt;&lt;/p&gt;





&lt;p&gt;You can view and easily copy the task content of the failed batch import task, to analyze it separately: in your IDE or using AI.&lt;/p&gt;





&lt;p&gt;If you want to find out when/how a specific entity was imported - you can perform a search using it's ERC field, and see the task details:&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126385388" src="https://liferay.dev/documents/portlet_file_entry/14/00-search.png/fd8eb602-efa2-e6d1-3f84-6a516e7c18b1"&gt;&lt;/p&gt;





&lt;p&gt; &lt;/p&gt;





&lt;h2&gt;Summary&lt;/h2&gt;





&lt;p&gt;For any project where Liferay is on the receiving end of a data pipeline it doesn't control — PIM systems, ERPs, legacy migrations — batch import failures aren't a matter of if, they're a matter of when. Batch Import Audit doesn't change how the Batch Engine works; it just makes the information the Batch Engine was already recording actually visible, so troubleshooting a failed import is a filter and a click instead of a support ticket and a database query.&lt;/p&gt;









&lt;p&gt;If you're dealing with the same kind of integration setup and this looks useful, the code is available in this &lt;a
    href="https://github.com/vitaliy-koshelenko/liferay-batch-import-audit"
    rel="noopener noreferrer" target="_blank"&gt;repo&lt;/a&gt; — take a look, adapt it, or open an issue with questions, feedback, or ideas for where it should go next.&lt;/p&gt;





&lt;p&gt; &lt;/p&gt;





&lt;p&gt;Leave your comments and feedback.&lt;/p&gt;





&lt;p&gt;Stand with Ukraine   &lt;img data-fileentryid="121505040"
    src="https://liferay.dev/documents/portlet_file_entry/14/Flag_of_Ukraine.png/1cb13ab8-9b87-bf67-380b-aec36a7f1c34"
    style="height: auto;width: 48.0px;" width="48"&gt;&lt;/p&gt;





&lt;p&gt;Enjoy &#x1f60f;&lt;/p&gt;





&lt;p&gt;  &lt;/p&gt;





&lt;div class="text-right"&gt;
&lt;p&gt;&lt;a class="btn btn-unstyled "
      href="https://www.linkedin.com/in/vitaliy-koshelenko"&gt;&lt;strong&gt;Vitaliy Koshelenko&lt;/strong&gt; &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Liferay DXP Architect at Solteq&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a
      href="mailto:vitalii.koshelenko@solteq.com"&gt;vitalii.koshelenko@solteq.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a
      href="https://www.solteq.com/" rel="noopener noreferrer"
        target="_blank"&gt;&lt;img data-fileentryid="125270663"
        src="https://liferay.dev/documents/portlet_file_entry/14/solteq-logo.jpg/daafc604-2a62-74e7-1f26-0e6d63b7be9e"
        style="height: auto;width: 219.0px;" width="219"&gt;&lt;/a&gt;&lt;br&gt;
&lt;br&gt;
 &lt;/p&gt;

&lt;p&gt; &lt;/p&gt;
&lt;/div&gt;</summary>
    <dc:creator>Vitalii Koshelenko</dc:creator>
    <dc:date>2026-07-29T09:07:00Z</dc:date>
  </entry>
  <entry>
    <title>Installing Liferay DXP 2026.Q1 on Red Hat JBoss Web Server 6.2.3</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126381472" />
    <author>
      <name>David H Nebinger</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126381472</id>
    <updated>2026-07-30T16:59:19Z</updated>
    <published>2026-07-28T19:28:00Z</published>
    <summary type="html">&lt;p&gt;Liferay’s official Tomcat installation guide is a useful starting point, but it is not quite enough for installing Liferay DXP 2026.Q1 on Red Hat JBoss Web Server 6.2.3.&lt;/p&gt;





&lt;p&gt;The reason is straightforward: JBoss Web Server is Tomcat, but it is Red Hat’s supported Tomcat distribution, and the current Liferay documentation contains a few assumptions that do not line up cleanly with a modern 2026.Q1 install.&lt;/p&gt;





&lt;div class="overflow-auto portlet-msg-info"&gt;I've reported these issues, so by the time you go looking, they might be fixed...&lt;/div&gt;





&lt;p&gt;In this post, we will install Liferay DXP 2026.Q1 on JWS 6.2.3 using RHEL 10 as the reference operating system. The same pattern applies to other Linux distributions, but the package manager and service integration details will vary.&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126381482" src="https://liferay.dev/documents/portlet_file_entry/14/2026-07-28_14-49-23.png/08bd53b7-f7e9-ce82-c76e-a8712c7c30bd"&gt;&lt;/p&gt;









&lt;figure&gt;
&lt;figcaption&gt;
&lt;p&gt;Liferay DXP 2026.Q1 download options: WAR, OSGi dependencies, admin tools, and the Tomcat bundle.&lt;/p&gt;

&lt;div class="overflow-auto portlet-msg-alert"&gt;Well, that was the plan until I found you can't get a developer download of JWS. Instead, we're going to use Apache Tomcat 10.1.57 and rename the directory to JWS to pretend it's the real deal.&lt;/div&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;





&lt;h2&gt;Why JBoss Web Server?&lt;/h2&gt;





&lt;p&gt;When someone asks for “JBoss” in the context of a Tomcat-based Liferay deployment, they may not mean JBoss EAP. They may mean Red Hat JBoss Web Server (JWS).&lt;/p&gt;





&lt;p&gt;JBoss Web Server is a separate Red Hat middleware product. Under the covers, JWS 6.x includes Apache Tomcat 10.1, Apache HTTP Server integration options, production-ready connectors such as &lt;code&gt;mod_cluster&lt;/code&gt;, Tomcat native libraries, APR, OpenSSL integration, and Red Hat’s middleware support lifecycle.&lt;/p&gt;





&lt;p&gt;This matters for enterprise environments because many organizations standardize on Red Hat-supported middleware even when the upstream runtime is Apache Tomcat. They may need:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;A fully supported Tomcat 10.1 runtime.&lt;/li&gt;
	&lt;li&gt;Longer enterprise compliance windows.&lt;/li&gt;
	&lt;li&gt;Red Hat support channels.&lt;/li&gt;
	&lt;li&gt;Supported Apache HTTP Server connector integration.&lt;/li&gt;
	&lt;li&gt;FIPS or hardened runtime requirements.&lt;/li&gt;
	&lt;li&gt;Standardized deployment through RHEL subscriptions, archives, or container images.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;For RHEL 10, Red Hat also provides a Tomcat 10.1 package directly through the operating system. JWS is still relevant when the client’s middleware standard is Red Hat Middleware Runtimes rather than base RHEL packages.&lt;/p&gt;





&lt;h2&gt;Version Alignment&lt;/h2&gt;





&lt;p&gt;Before touching files, get the compatibility story clear.&lt;/p&gt;





&lt;p&gt;Liferay DXP 2026.Q1 supports Tomcat 10.1 and RHEL 10. JWS 6.x provides Tomcat 10.1, so the servlet container version lines up.&lt;/p&gt;





&lt;p&gt;The Java version is where older documentation can mislead you. The Liferay Tomcat install page currently shows a Java 8 example in &lt;code&gt;setenv.sh&lt;/code&gt;. Do not use that for 2026.Q1. For this installation, use JDK 21.&lt;/p&gt;





&lt;p&gt;JWS 6.x supports multiple modern Java versions, but Liferay 2026.Q1 should be run on JDK 21. Do not let the server default to a newer Java such as JDK 25, and do not copy older Java 8 examples from older Liferay documentation.&lt;/p&gt;





&lt;div class="overflow-auto portlet-msg-error"&gt;Liferay 2026.Q1+ is &lt;em&gt;&lt;strong&gt;only&lt;/strong&gt;&lt;/em&gt; compatible with JDK 21, no exceptions.&lt;/div&gt;





&lt;p&gt;On RHEL 10:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;$ sudo dnf install java-21-openjdk-headless unzip tar
$ sudo alternatives --config java
$ java -version&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;On other distributions, the equivalent commands are usually:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;# Debian or Ubuntu
$ sudo apt install openjdk-21-jdk-headless unzip tar

# SUSE
$ sudo zypper install java-21-openjdk-headless unzip tar

# Fedora
$ sudo dnf install java-21-openjdk-headless unzip tar&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;The important part is not the package manager. The important part is that the runtime used by JWS is Java 21.&lt;/p&gt;





&lt;h2&gt;Downloads&lt;/h2&gt;





&lt;p&gt;Download these Liferay files:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;DXP WAR&lt;/li&gt;
	&lt;li&gt;OSGi Dependencies&lt;/li&gt;
	&lt;li&gt;Admin Tools&lt;/li&gt;
	&lt;li&gt;Bundled with Tomcat&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;The Tomcat bundle is not the runtime we are deploying, but it is still useful. It shows the directory structure, scripts, &lt;code&gt;ROOT.xml&lt;/code&gt;, and runtime assumptions Liferay expects when it owns the Tomcat layout. All of the locations, contents, etc for the Liferay Tomcat bundle works, so if you have local issues, comparing your installation to the Liferay Tomcat bundle can often expose differences that might be contributing to the issues.&lt;/p&gt;





&lt;p&gt;For separate installation of JWS, use the Red Hat Customer Portal:&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Create or use a Red Hat Developer account.&lt;/li&gt;
	&lt;li&gt;Confirm that your account has access to the no-cost Individual Developer Subscription or the client’s Middleware Runtimes subscription.&lt;/li&gt;
	&lt;li&gt;Go to Red Hat Customer Portal downloads.&lt;/li&gt;
	&lt;li&gt;Select Red Hat JBoss Web Server.&lt;/li&gt;
	&lt;li&gt;Download the JWS 6.2.3 application server archive.&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;I'm testing locally on my Mac, so the generic JWS application server ZIP is usually enough because the Tomcat runtime itself is Java-based. For other Linux platforms, you might want the native component downloads if you are specifically testing platform-native Apache HTTP Server integration, APR, OpenSSL, or connector behavior.&lt;/p&gt;





&lt;h2&gt;Target Layout&lt;/h2&gt;





&lt;p&gt;The official Liferay Tomcat page assumes that Liferay Home is the parent directory of the Tomcat folder. That works for the Liferay-provided Tomcat bundle, but it is not always the best fit for JWS.&lt;/p&gt;





&lt;p&gt;For this install, use explicit paths:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;/opt/jws-6.2.3              JBoss Web Server installation
/opt/jws-6.2.3/tomcat       Tomcat runtime
/opt/liferay                Liferay Home&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;As indicated earlier, I'm using Apache Tomcat 10.1 since I can't get a JWS download. I manually created the /opt/jws-6.2.3 directory, exploded the apache-tomcat-10.1.57.tar.gz file I downloaded and renamed the directory to tomcat to match what JWS would have.&lt;/p&gt;





&lt;p&gt;I prefer &lt;code&gt;/opt/liferay&lt;/code&gt; for Liferay Home because it keeps Liferay’s mutable runtime data separate from the application server installation. Because of the runtime directory updates, you might also consider &lt;code&gt;/var/run/liferay&lt;/code&gt; or something similar.&lt;/p&gt;





&lt;p&gt;Create the Liferay Home structure:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;$ sudo mkdir -p /opt/liferay/{data,deploy,license}
$ sudo mkdir -p /opt/liferay/{logs,osgi,tools}
$ sudo chown -R tomcat:tomcat /opt/liferay&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;If the &lt;code&gt;tomcat&lt;/code&gt; user does not exist yet, create it first:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;$ sudo groupadd -g 53 -r tomcat
$ sudo useradd -c &amp;quot;tomcat&amp;quot; -u 53 -g tomcat \
  -s /sbin/nologin -r tomcat&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Red Hat’s JWS post-install scripts can also create and configure the service user when using the archive installation flow.&lt;/p&gt;





&lt;h2&gt;Install JBoss Web Server&lt;/h2&gt;





&lt;p&gt;Extract the archive:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;$ sudo unzip jws-6.2.3-application-server.zip -d /opt
$ sudo chown -R tomcat:tomcat /opt/jws-6.2.3&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Set a few working variables while installing:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;$ export JWS_HOME=/opt/jws-6.2.3
$ export CATALINA_HOME=$JWS_HOME/tomcat
$ export CATALINA_BASE=$CATALINA_HOME
$ export LIFERAY_HOME=/opt/liferay&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;If you want &lt;code&gt;systemd&lt;/code&gt; management on RHEL, run the JWS post-install script:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;$ cd /opt/jws-6.2.3/tomcat
$ sudo sh .postinstall.systemd
$ sudo systemctl enable jws6-tomcat.service&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;If you're using your package manager to install JWS, you may actually have pieces installed in separate locations. The &lt;code&gt;CATALINA_HOME&lt;/code&gt; environment variable where the static files and binaries are installed and &lt;code&gt;CATALINA_BASE&lt;/code&gt; environment variable where dynamic, changeable components are installed (like the web apps that are deployed) will be set up by the installation, you won't have to set them yourself, but you'll have to know about them to install the right parts in the right places. It will also handle the &lt;code&gt;systemd&lt;/code&gt; service setup, but you'll need to do the service enabling.&lt;/p&gt;





&lt;p&gt;You'll still do the &lt;code&gt;LIFERAY_HOME&lt;/code&gt; setup stuff.&lt;/p&gt;





&lt;h3&gt;JWS Tomcat Cleanup&lt;/h3&gt;





&lt;p&gt;We're going to be replacing the &lt;code&gt;ROOT&lt;/code&gt; web application with Liferay's app, so if there's an &lt;code&gt;$CATALINA_BASE/webapps/ROOT&lt;/code&gt; folder, you're going to want to delete it. Check out the other web apps that might be in that folder, you may want to purge those as well.&lt;/p&gt;





&lt;h2&gt;Install Liferay Files&lt;/h2&gt;





&lt;p&gt;Expand the OSGi dependencies into Liferay Home:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;$ sudo unzip liferay-dxp-osgi-*.zip -d /opt/liferay/osgi
$ sudo chown -R tomcat:tomcat /opt/liferay/osgi&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Install the tools somewhere predictable:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;$ sudo unzip liferay-dxp-tools-*.zip \
  -d /opt/liferay
$ sudo chown -R tomcat:tomcat /opt/liferay/tools&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;If you have artifacts to deploy such as a DXP license file, OSGi modules or portlet war files, themes, etc. you can put them in &lt;code&gt;/opt/liferay/deploy&lt;/code&gt; for Liferay to pick up when it starts.&lt;/p&gt;





&lt;p&gt;Now deploy the DXP WAR as the root web application:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;$ sudo rm -rf $CATALINA_BASE/webapps/ROOT
$ sudo mkdir -p $CATALINA_BASE/webapps/ROOT
$ sudo unzip liferay-dxp-*.war -d $CATALINA_BASE/webapps/ROOT
$ sudo chown -R tomcat:tomcat $CATALINA_BASE/webapps/ROOT&lt;/code&gt;
&lt;/pre&gt;





&lt;h2&gt;Configure Liferay Home Explicitly&lt;/h2&gt;





&lt;p&gt;Do not rely on Liferay guessing the correct home directory.&lt;/p&gt;





&lt;p&gt;Create &lt;code&gt;/opt/liferay/portal-ext.properties&lt;/code&gt;:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;liferay.home=/opt/liferay&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Then pass the same value as a JVM system property (in &lt;code&gt;setenv.sh&lt;/code&gt; as shown below). This avoids startup timing problems and makes the location obvious from the process definition:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;-Dliferay.home=/opt/liferay&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;In production, &lt;code&gt;portal-ext.properties&lt;/code&gt; will usually also contain database settings, mail settings, clustering-related settings, and environment-specific configuration.&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;liferay.home=/opt/liferay

jdbc.default.driverClassName=org.postgresql.Driver
jdbc.default.url=jdbc:postgresql://db.example.com:5432/lportal
jdbc.default.username=liferay
jdbc.default.password=change-me&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Do not use the bundled Hypersonic database outside local testing.&lt;/p&gt;





&lt;h2&gt;Configure &lt;code&gt;setenv.sh&lt;/code&gt;&lt;/h2&gt;





&lt;p&gt;This is one of the places where the official documentation is incomplete for modern Java.&lt;/p&gt;





&lt;p&gt;Your JWS Tomcat &lt;code&gt;bin/setenv.sh&lt;/code&gt; should include the normal Liferay memory and runtime options, but it also needs the Java module access flags required by Liferay on Java 21.&lt;/p&gt;





&lt;p&gt;Create or update:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;/opt/jws-6.2.3/tomcat/bin/setenv.sh&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Use this as a starting point:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;# Ensure use of JDK21 for Liferay/Tomcat
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk
export PATH=&amp;quot;$JAVA_HOME/bin:$PATH&amp;quot;

# Set this to your real liferay home. Exporting as
# an environment var ensures visibility within the
# running process.
export LIFERAY_HOME=/opt/liferay

CATALINA_OPTS=&amp;quot;$CATALINA_OPTS -Dfile.encoding=UTF-8&amp;quot;
CATALINA_OPTS=&amp;quot;$CATALINA_OPTS -Djava.net.preferIPv4Stack=true&amp;quot;
CATALINA_OPTS=&amp;quot;$CATALINA_OPTS -Duser.timezone=GMT&amp;quot;
CATALINA_OPTS=&amp;quot;$CATALINA_OPTS -Dliferay.home=$LIFERAY_HOME&amp;quot;

CATALINA_OPTS=&amp;quot;$CATALINA_OPTS -Xms2560m -Xmx2560m&amp;quot;
CATALINA_OPTS=&amp;quot;$CATALINA_OPTS -XX:NewSize=1536m&amp;quot; 
CATALINA_OPTS=&amp;quot;$CATALINA_OPTS -XX:MaxNewSize=1536m&amp;quot;
CATALINA_OPTS=&amp;quot;$CATALINA_OPTS -XX:MetaspaceSize=768m&amp;quot;
CATALINA_OPTS=&amp;quot;$CATALINA_OPTS -XX:MaxMetaspaceSize=768m&amp;quot;
CATALINA_OPTS=&amp;quot;$CATALINA_OPTS -XX:SurvivorRatio=7&amp;quot;

export JDK_JAVA_OPTIONS=&amp;quot;${JDK_JAVA_OPTIONS} \
 --add-opens=java.base/java.lang=ALL-UNNAMED \
 --add-opens=java.base/java.lang.invoke=ALL-UNNAMED \
 --add-opens=java.base/java.lang.reflect=ALL-UNNAMED \
 --add-opens=java.base/java.net=ALL-UNNAMED \
 --add-opens=java.base/java.util=ALL-UNNAMED \
 --add-opens=java.base/sun.net.www.protocol.http=ALL-UNNAMED \
 --add-opens=java.base/sun.net.www.protocol.https=ALL-UNNAMED \
 --add-opens=java.base/sun.util.calendar=ALL-UNNAMED \
 --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED \
 --add-opens=jdk.zipfs/jdk.nio.zipfs=ALL-UNNAMED&amp;quot;&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Make it executable:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;$ sudo chmod 750 /opt/jws-6.2.3/tomcat/bin/setenv.sh
$ sudo chown tomcat:tomcat /opt/jws-6.2.3/tomcat/bin/setenv.sh&lt;/code&gt;&lt;/pre&gt;





&lt;div class="overflow-auto portlet-msg-alert"&gt;The &lt;code&gt;--add-opens&lt;/code&gt; arguments are not optional noise. Without them, Liferay may start with reflective access warnings or fail later when a code path hits Java’s stronger module boundaries.&lt;/div&gt;





&lt;p&gt;Two things to note here...&lt;/p&gt;





&lt;p&gt;First, the memory settings are just what Liferay uses for a vanilla demo environment. You may need to increase those to handle your expected capacity and load. Liferay provides a performance white paper where they provide a suitable starting set of JVM parameters for a production environment, but of course load testing and tuning are necessary to get the right settings for your environment.&lt;/p&gt;









&lt;p&gt;Secondly, if you're using the &lt;code&gt;systemd&lt;/code&gt; service to start/stop JWS, you might expect that you'd manage the environment and JVM parameters &amp;quot;the &lt;code&gt;systemd&lt;/code&gt; way&amp;quot;, but you'd be wrong. Per the &lt;a
    href="https://docs.redhat.com/en/documentation/red_hat_jboss_web_server/6.2/html-single/installation_guide/index" rel="noopener noreferrer"&gt;Red Hat JWS 6.2 Installation Guide&lt;/a&gt;, you still apply Tomcat environment variables and JVM parameters in the &lt;code&gt;setenv.sh&lt;/code&gt; script. &lt;/p&gt;





&lt;h2&gt;Configure Tomcat for Liferay&lt;/h2&gt;





&lt;p&gt;The Liferay Tomcat bundle is useful here. Compare its configuration files against the JWS Tomcat configuration and copy the relevant Liferay-specific pieces.&lt;/p&gt;





&lt;p&gt;The important items are:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;code&gt;conf/Catalina/localhost/ROOT.xml&lt;/code&gt;&lt;/li&gt;
	&lt;li&gt;&lt;code&gt;conf/catalina.properties&lt;/code&gt;&lt;/li&gt;
	&lt;li&gt;&lt;code&gt;conf/server.xml&lt;/code&gt;&lt;/li&gt;
	&lt;li&gt;&lt;code&gt;conf/web.xml&lt;/code&gt;&lt;/li&gt;
	&lt;li&gt;&lt;code&gt;conf/logging.properties&lt;/code&gt;&lt;/li&gt;
	&lt;li&gt;scripts in &lt;code&gt;bin/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;The most important JWS-specific check is &lt;code&gt;conf/catalina.properties&lt;/code&gt;.&lt;/p&gt;





&lt;p&gt;Liferay’s &lt;code&gt;support-tomcat.jar&lt;/code&gt; must be visible to Tomcat’s common class loader. Add it at the beginning of &lt;code&gt;common.loader&lt;/code&gt;:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;common.loader=&amp;quot;${catalina.home}/webapps/ROOT/WEB-INF/
  lib/support-tomcat.jar&amp;quot;,...&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Also confirm that the HTTP connector in &lt;code&gt;server.xml&lt;/code&gt; uses UTF-8:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;&amp;lt;Connector
    port=&amp;quot;8080&amp;quot;
    protocol=&amp;quot;HTTP/1.1&amp;quot;
    connectionTimeout=&amp;quot;20000&amp;quot;
    redirectPort=&amp;quot;8443&amp;quot;
    URIEncoding=&amp;quot;UTF-8&amp;quot; /&amp;gt;&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;If you are fronting JWS with Apache HTTP Server, a load balancer, or OpenShift routing, also review proxy headers, secure scheme handling, and the connector strategy before putting the node behind real traffic.&lt;/p&gt;





&lt;h2&gt;Start and Validate&lt;/h2&gt;





&lt;p&gt;Start JWS:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;$ sudo systemctl start jws6-tomcat.service
$ sudo systemctl status jws6-tomcat.service&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Watch the logs:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;$ sudo tail -f /opt/jws-6.2.3/tomcat/logs/catalina.out&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Confirm these items during first startup:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;Java reports version 21.&lt;/li&gt;
	&lt;li&gt;Liferay Home resolves to &lt;code&gt;/opt/liferay&lt;/code&gt;.&lt;/li&gt;
	&lt;li&gt;OSGi modules are loaded from &lt;code&gt;/opt/liferay/osgi&lt;/code&gt;.&lt;/li&gt;
	&lt;li&gt;The database connection is not using Hypersonic in production.&lt;/li&gt;
	&lt;li&gt;No startup failure points to missing Java module access.&lt;/li&gt;
	&lt;li&gt;&lt;code&gt;support-tomcat.jar&lt;/code&gt; is visible through &lt;code&gt;common.loader&lt;/code&gt;.&lt;/li&gt;
	&lt;li&gt;The portal is deployed as &lt;code&gt;ROOT&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;After logging in, confirm the resolved Liferay Home path from Control Panel:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;Global Menu -&amp;gt; Control Panel -&amp;gt; Server Administration 
  -&amp;gt; Properties -&amp;gt; System Properties&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;Search for:&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;liferay.home&lt;/code&gt;&lt;/pre&gt;





&lt;p&gt;It should point to &lt;code&gt;/opt/liferay&lt;/code&gt;.&lt;/p&gt;





&lt;h2&gt;Operational Notes&lt;/h2&gt;





&lt;p&gt;For production, the application server install is only part of the work.&lt;/p&gt;





&lt;p&gt;You still need to decide how to manage:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;Database backup and restore.&lt;/li&gt;
	&lt;li&gt;Document Library storage.&lt;/li&gt;
	&lt;li&gt;Search engine configuration.&lt;/li&gt;
	&lt;li&gt;TLS termination.&lt;/li&gt;
	&lt;li&gt;Reverse proxy headers.&lt;/li&gt;
	&lt;li&gt;Session strategy (Sticky sessions still recommended).&lt;/li&gt;
	&lt;li&gt;Clustering.&lt;/li&gt;
	&lt;li&gt;Marketplace and hot deploy policies.&lt;/li&gt;
	&lt;li&gt;Patch and quarterly update workflow.&lt;/li&gt;
	&lt;li&gt;SELinux context and filesystem permissions.&lt;/li&gt;
	&lt;li&gt;Systemd restart behavior and memory limits.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;On RHEL systems with SELinux enabled, do not treat permissions as only Unix ownership. Validate SELinux contexts for any directory JWS needs to read or write, especially &lt;code&gt;/opt/liferay&lt;/code&gt;, log directories, custom mounts, and shared storage.&lt;/p&gt;





&lt;h2&gt;Tradeoffs&lt;/h2&gt;





&lt;p&gt;Installing Liferay on JWS gives you a supported enterprise Tomcat runtime, but it also means you own more of the assembly.&lt;/p&gt;





&lt;p&gt;The Liferay Tomcat bundle is easier because Liferay has already made the runtime decisions for you. JWS is more appropriate when the client’s platform standards require Red Hat middleware support, Red Hat packaging, or a supported Tomcat distribution that fits existing operational controls.&lt;/p&gt;





&lt;p&gt;The cost is that you must be explicit:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;Explicit Java version.&lt;/li&gt;
	&lt;li&gt;Explicit Liferay Home.&lt;/li&gt;
	&lt;li&gt;Explicit JVM options.&lt;/li&gt;
	&lt;li&gt;Explicit Tomcat common class loader configuration.&lt;/li&gt;
	&lt;li&gt;Explicit ownership and service management.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;That is not a bad tradeoff, but it should be intentional.&lt;/p&gt;





&lt;p&gt;Upgrades&lt;/p&gt;





&lt;p&gt;So this is a considerable tradeoff. Using a Liferay Tomcat Bundle or Docker Image makes upgrading Liferay software &amp;quot;easy&amp;quot; (excludes work necessary for addressing your own customizations).&lt;/p&gt;





&lt;p&gt;Under JWS, you're going to be doing much of this manually, but its not too complicated.&lt;/p&gt;





&lt;p&gt;You still download the Liferay DXP war file, the OSGi dependencies zip and the tools zip.&lt;/p&gt;





&lt;p&gt;Delete the contents of the &lt;code&gt;$CATALINA_BASE/webapps/ROOT&lt;/code&gt; dir and expand the new war file like we did above.&lt;/p&gt;





&lt;p&gt;The OSGi dependencies zip has to be handled with care. Delete the &lt;code&gt;marketplace&lt;/code&gt;, &lt;code&gt;portal&lt;/code&gt;, &lt;code&gt;portal-war&lt;/code&gt;, &lt;code&gt;state&lt;/code&gt; and &lt;code&gt;static&lt;/code&gt; directories from &lt;code&gt;/opt/liferay/osgi&lt;/code&gt;. The other folders will contain your own artifacts, you don't want to delete them or you discard your own deployed work. Unzip the OSGi dependencies zip like we did above.&lt;/p&gt;





&lt;p&gt;Tools will also need to be handled with care. Your property files in the various /opt/liferay/tools directory will get clobbered when you unzip the tools zip. Copy them and restore them after unzipping the tools zip file.&lt;/p&gt;





&lt;p&gt;You might also want to check the Liferay Tomcat bundle for changes in other key files like &lt;code&gt;setenv.sh&lt;/code&gt;, &lt;code&gt;catalina.properties&lt;/code&gt;, etc.&lt;/p&gt;





&lt;h2&gt;Final Thoughts&lt;/h2&gt;





&lt;p&gt;The main mistake to avoid is treating the Liferay Tomcat documentation as a complete JWS installation guide. It is a good reference, but for Liferay DXP 2026.Q1 on JWS 6.2.3 you need to correct three assumptions:&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Use JDK 21, not the older Java examples.&lt;/li&gt;
	&lt;li&gt;Add the required Java &lt;code&gt;--add-opens&lt;/code&gt; options.&lt;/li&gt;
	&lt;li&gt;Define Liferay Home explicitly instead of relying on the Tomcat parent directory.&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;Once those are handled, the deployment model is familiar: JWS supplies Tomcat 10.1, Liferay supplies the WAR and OSGi dependencies, and &lt;code&gt;/opt/liferay&lt;/code&gt; becomes the stable runtime home for configuration, modules, logs, data, and operational tooling.&lt;/p&gt;





&lt;h2&gt;References&lt;/h2&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;a
      href="https://learn.liferay.com/w/dxp/self-hosted-installation-and-upgrades/installing-liferay-on-a-local-server/installing-liferay-on-an-application-server/installing-on-tomcat"&gt;Liferay Tomcat install guide&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a
      href="https://support.liferay.com/w/liferay-dxp-2026.q1-lts-compatibility-matrix"&gt;Liferay DXP 2026.Q1 compatibility matrix&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a
      href="https://learn.liferay.com/w/dxp/self-hosted-installation-and-upgrades/reference/jvm-configuration"&gt;Liferay JVM configuration&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a
      href="https://learn.liferay.com/w/dxp/self-hosted-installation-and-upgrades/reference/liferay-home"&gt;Liferay Home reference&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;a href="https://docs.redhat.com/en/documentation/red_hat_jboss_web_server/6.2/html-single/installation_guide/index"&gt;Red Hat JBoss Web Server 6.2 installation guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</summary>
    <dc:creator>David H Nebinger</dc:creator>
    <dc:date>2026-07-28T19:28:00Z</dc:date>
  </entry>
  <entry>
    <title>Beyond the Prototype: What’s New in the Liferay Fragments Repository</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126355026" />
    <author>
      <name>Peter Richards</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126355026</id>
    <updated>2026-07-23T13:49:00Z</updated>
    <published>2026-07-23T07:12:00Z</published>
    <summary type="html">&lt;h3&gt;The Evolution of the Library&lt;/h3&gt;





&lt;p data-path-to-node="6"&gt;When I wrote about &lt;a data-hveid="0"
    data-ved="0CAAQ_4QMahgKEwj8643mnuiVAxUAAAAAHQAAAAAQhwE"
    href="https://liferay.dev/b/accelerating-liferay-fragments-with-ai-automation"
    rel="noopener noreferrer" target="_blank"&gt;scaling Liferay fragments with AI and automation&lt;/a&gt; earlier this year, the repository was already transitioning from a experimental lab into a structured UX library. The goal was simple: prove that custom fragment design could be accelerated using AI tools without sacrificing maintainability or performance.&lt;/p&gt;





&lt;p data-path-to-node="7"&gt;Since then, the repository has expanded significantly in both scope and capability. What started as a modest collection of components and navigational experiments has grown into a comprehensive, community-driven resource for Sales Engineers, solutions architects, and frontend developers looking to deliver polished, production-ready Liferay DXP experiences without building every component from scratch.&lt;/p&gt;





&lt;h3&gt;Key Enhancements at a Glance&lt;/h3&gt;





&lt;p data-path-to-node="10"&gt;If you haven’t checked out the repo recently, here is what has changed behind the scenes:&lt;/p&gt;





&lt;h4&gt;1. Fully Verified for 2026 LTS&lt;/h4&gt;





&lt;p data-path-to-node="12"&gt;Every fragment in the collection has been systematically tested and validated against the latest &lt;b&gt;2026 LTS&lt;/b&gt; release. You can import these components into your modern environments knowing that markup, CSS classes, and Client Extension hooks are fully aligned with current platform standards.&lt;/p&gt;





&lt;h4&gt;2. Built-in Visual Gallery&lt;/h4&gt;





&lt;p data-path-to-node="14"&gt;Choosing the right fragment shouldn't require importing six different zip packages just to see how they render on a page. The repository now includes a &lt;b&gt;Visual Gallery&lt;/b&gt; right in the documentation, allowing you to quickly browse visual previews, review layout options, and select exact components before touching your DXP instance.&lt;/p&gt;





&lt;h4&gt;3. Complete Backwards Compatibility&lt;/h4&gt;





&lt;p data-path-to-node="16"&gt;While the primary branch targets 2026 LTS, real-world deployment often involves supporting legacy or client-specific environments. The repository now maintains packaged, version-specific release archives to ensure seamless backwards compatibility across prior DXP versions.&lt;/p&gt;





&lt;h4&gt;4. Instant &amp;quot;Drop-and-Go&amp;quot; Deployment&lt;/h4&gt;





&lt;p data-path-to-node="18"&gt;We’ve retained and refined our automated zip-based release workflow. Download the packaged release file, place it straight into your DXP &lt;code&gt;/deploy&lt;/code&gt; directory, and let the platform handle the rest — no manual import wizards required.&lt;/p&gt;





&lt;h3&gt;Featured Fragments to Explore&lt;/h3&gt;





&lt;p data-path-to-node="21"&gt;Here are a few of the standout components currently available in the collection:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;
	&lt;p
      data-path-to-node="22,0,0"&gt;&lt;b&gt;Automated Meter Reading:&lt;/b&gt; A utility fragment built for industrial and utility use cases. It handles numeric input formatting, backspace logic, and auto-advancement out of the box, pushing data directly to Liferay Objects via callback functions.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p
      data-path-to-node="22,1,0"&gt;&lt;b&gt;Commerce Dynamic Badge Overlay:&lt;/b&gt; A real-time product overlay fragment that dynamically applies badges (such as &lt;i&gt;New&lt;/i&gt;, &lt;i&gt;Low Stock&lt;/i&gt;, or &lt;i&gt;Featured&lt;/i&gt;) over Commerce product card components based on live data triggers.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p data-path-to-node="22,2,0"&gt;&lt;b&gt;Collection-Based Date Displays:&lt;/b&gt; Configurable date-mapping UI components that format and bind complex temporal data directly from asset collections and data providers.&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;





&lt;h3&gt;How to Get Involved&lt;/h3&gt;





&lt;p data-path-to-node="25"&gt;This repository thrives when it reflects real customer scenarios and field requirements. Whether you’re preparing a high-impact demo or building out a custom portal, there are several ways to contribute:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;
	&lt;p
      data-path-to-node="26,0,0"&gt;&lt;b&gt;Submit Pull Requests:&lt;/b&gt; Built a custom fragment for a recent prospect or client solution? Open a PR and share it with the wider ecosystem.&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p data-path-to-node="26,1,0"&gt;&lt;b&gt;Suggest New Features:&lt;/b&gt; Have an idea for a component that would solve a common demo hurdle? Open an issue on GitHub or reach out directly with your concept.&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;





&lt;h3&gt;Get Started Today&lt;/h3&gt;





&lt;p data-path-to-node="29"&gt;Stop reinventing the wheel on every project. Take a look through the visual gallery, grab the latest release, and drop these components into your next environment:&lt;/p&gt;





&lt;p data-path-to-node="30"&gt;&#x1f449; &lt;b&gt;Explore the Repository:&lt;/b&gt; &lt;a
    data-hveid="0"
    data-ved="0CAAQ_4QMahgKEwj8643mnuiVAxUAAAAAHQAAAAAQiAE"
    href="https://github.com/peterrichards-lr/liferay-fragments"
    rel="noopener noreferrer" target="_blank"&gt;github.com/peterrichards-lr/liferay-fragments&lt;/a&gt;&lt;/p&gt;</summary>
    <dc:creator>Peter Richards</dc:creator>
    <dc:date>2026-07-23T07:12:00Z</dc:date>
  </entry>
  <entry>
    <title>Liferay AI Hub Practical Recommendations: Agents, Instructions, Prompts &amp; Guardrails</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126354004" />
    <author>
      <name>David H Nebinger</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126354004</id>
    <updated>2026-07-22T16:23:41Z</updated>
    <published>2026-07-22T15:14:00Z</published>
    <summary type="html">&lt;p&gt;I've recently been working on a project to create a Chatbot using AI Hub. Although the documentation at &lt;a
    href="https://learn.liferay.com/w/ai-hub/integrating-ai-hub-with-liferay-dxp" rel="noopener noreferrer"&gt;https://learn.liferay.com/w/ai-hub/integrating-ai-hub-with-liferay-dxp&lt;/a&gt; is very helpful at completing this task, there are some things that it leaves out. I thought these might be helpful to others, so I thought I'd share them here.&lt;/p&gt;





&lt;h2&gt;1. The building blocks (quick orientation)&lt;/h2&gt;





&lt;p&gt;When you're using AI Hub, you're going to be defining a bunch of different things:&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Agent&lt;/strong&gt; — a single task with its own workflow (a canvas of nodes). Built-in agents (Change Tone, Fix Spelling and Grammar, Improve Writing, Liferay Search, Make Longer, Make Shorter) are read-only; custom agents are yours to edit. An agent has input variable(s), one output variable, an optional set of data sources, and optional guardrails.&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Instruction&lt;/strong&gt; — a reusable block of guidance applied at runtime across many agents (tone, brand voice, policy). It carries a rule plus the response to give when the rule fires, and a scope (Everywhere / CMS only / Click-to-Chat only).&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Guardrail&lt;/strong&gt; — an enforced safety check that runs outside the model, so it holds even when a prompt tries to talk around it. Input guardrails screen the user message; output guardrails screen the model reply. Guardrails attach per-agent.&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Data source&lt;/strong&gt; — a crawled website (same domain, up to 3 link levels deep, max 500 pages) that agents query by semantic search. Attach to the agents that need grounded answers.&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Supervisor (AI Assistant / Chatbot)&lt;/strong&gt; — the router. The AI Assistant lives in the CMS editor; a Chatbot is a deployable widget scoped to a chosen set of Assigned Agents. Either one reads the request and routes it to the best-matching agent(s), and can combine agents for one request.&lt;/p&gt;





&lt;div&gt;
&lt;div class="overflow-auto portlet-msg-alert"&gt;&lt;strong&gt;Titles and descriptions are functional, not cosmetic&lt;/strong&gt;&lt;br&gt;
&lt;br&gt;
Across all of AI Hub, an agent's title and description are the primary signal the supervisor uses to select it. Wherever a supervisor sits in front of agents (the CMS AI Assistant and every deployed chatbot alike), it compares the user's request against each available agent's title and description and routes to the best match. They are not just labels for humans: a vague or inaccurate description means the right agent may never be picked, while a sharp one that names the trigger and the result makes routing reliable. Treat writing them as part of building the agent, not an afterthought.&lt;/div&gt;
&lt;/div&gt;





&lt;h2&gt;2. Agent vs. Instruction: which one to build&lt;/h2&gt;





&lt;p&gt;This is the most common design decision. The rule of thumb: &lt;strong&gt;an agent does work; an instruction shapes how all work is done.&lt;/strong&gt;&lt;/p&gt;





&lt;table style="width: 100.0%;"&gt;
	&lt;thead&gt;
		&lt;tr&gt;
			&lt;th
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;text-align: left;width: 60.0%;"&gt;If…&lt;/th&gt;
			&lt;th
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;text-align: left;"&gt;Then…&lt;/th&gt;
		&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;The user needs a discrete task performed (translate, summarize, search, generate)&lt;/td&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Build an &lt;strong&gt;AGENT&lt;/strong&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;You want a consistent rule applied across many agents (brand tone, &amp;quot;never give financial advice&amp;quot;, always answer in the user's language)&lt;/td&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Write an &lt;strong&gt;INSTRUCTION&lt;/strong&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;The behavior needs its own inputs, output, data sources, or a multi-step workflow&lt;/td&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Build an &lt;strong&gt;AGENT&lt;/strong&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;The behavior is a one-line policy that shouldn't be copy-pasted into every prompt&lt;/td&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Write an &lt;strong&gt;INSTRUCTION&lt;/strong&gt;&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;You need enforcement that survives a cleverly worded prompt (PII, jailbreak, harmful output)&lt;/td&gt;
			&lt;td style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Add a &lt;strong&gt;GUARDRAIL&lt;/strong&gt; (not an instruction)&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;





&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; instructions are defined once and reused, so they keep tone and policy consistent without editing each agent. Reserve new agents for genuinely new capabilities — every custom agent is a maintained asset (and, on some tiers, a limited one, e.g. up to five custom agents on Activate).&lt;/p&gt;





&lt;h2&gt;3. One forking agent vs. several agents the supervisor picks between&lt;/h2&gt;





&lt;p&gt;When a job has several branches (e.g. translate to Spanish, French, or German), you can build one agent with a large branching workflow, or several small single-purpose agents that a chatbot / the AI Assistant routes between. &lt;strong&gt;Prefer several focused agents.&lt;/strong&gt;&lt;/p&gt;





&lt;h3&gt;Why separate agents usually win&lt;/h3&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Routing is what the supervisor is for.&lt;/strong&gt; The AI Assistant and chatbots already compare the request to each available agent and pick the best match — even combining two (translate + shorten) for one request. Small, clearly-described agents make that routing accurate.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Descriptions drive selection.&lt;/strong&gt; Each agent's title and description are how the supervisor decides. One giant forking agent has a single description, so the router can't see the branches inside it — a French request may never reach the French branch. Separate agents each advertise exactly what they do.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Simpler workflows, easier testing.&lt;/strong&gt; A focused workflow is easier to reason about, duplicate, and debug than one canvas with many conditional paths.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Independent governance.&lt;/strong&gt; Guardrails and data sources attach per-agent. Separate agents let you ground or lock down only the branch that needs it, instead of one agent carrying every rule.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Permissions and reuse.&lt;/strong&gt; Per-agent permissions and reuse across chatbots are cleaner when each capability is its own agent.&lt;/li&gt;
&lt;/ul&gt;





&lt;h3&gt;When a single forking workflow is justified&lt;/h3&gt;





&lt;ul&gt;
	&lt;li&gt;The branches are internal steps the user should never pick between (e.g. detect language → normalize → translate as one pipeline).&lt;/li&gt;
	&lt;li&gt;The branches share heavy setup (same data sources, same pre/post-processing) and splitting would duplicate a lot of workflow.&lt;/li&gt;
	&lt;li&gt;You're near an agent-count limit and the branches are minor variations of one task.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;&lt;strong&gt;Practical pattern:&lt;/strong&gt; build one small agent per user-visible capability, give each a sharp description, assign the relevant set to a chatbot (or let the CMS AI Assistant see them), and let the supervisor route. Use forking inside an agent only for steps the user shouldn't have to name.&lt;/p&gt;





&lt;h2&gt;4. How to formulate an agent's prompt&lt;/h2&gt;





&lt;p&gt;An agent node has a &lt;strong&gt;Prompt&lt;/strong&gt; (the LLM's standing instruction) and a &lt;strong&gt;User Message&lt;/strong&gt; template that injects inputs via placeholders like &lt;code&gt;{{text}}&lt;/code&gt;. Liferay's own example is a strong template — it works because it stacks four things:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Role&lt;/strong&gt; — &amp;quot;You are an expert translator.&amp;quot; Give the model a clear persona.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Scope&lt;/strong&gt; — &amp;quot;Your sole task is to translate the provided text to Spanish…&amp;quot; State the one job and bound it.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Edge cases&lt;/strong&gt; — &amp;quot;If the text is already in Spanish, return it unchanged.&amp;quot; Name the tricky inputs and say what to do.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Output format&lt;/strong&gt; — &amp;quot;Output only the translated text, with no explanations or commentary.&amp;quot; Constrain the shape of the reply.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;&lt;strong&gt;Recommendations:&lt;/strong&gt; keep one job per agent (matches the routing model); write the prompt as standing behavior and put the variable content in the User Message template; reference inputs by their exact variable names; specify the output format explicitly (the supervisor may chain your output into another agent, so clean, comment-free output matters); and don't bake policy that belongs in a reusable instruction or a guardrail into the prompt.&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;Naming for routing:&lt;/strong&gt; the description isn't decoration — it's how the supervisor finds the agent. Write descriptions that name the trigger and the result (&amp;quot;Translates text to French&amp;quot;) rather than vague labels (&amp;quot;Language helper&amp;quot;).&lt;/p&gt;





&lt;h2&gt;5. How to write instructions&lt;/h2&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;State both halves.&lt;/strong&gt; Every instruction is a rule plus the response when it fires — e.g. &amp;quot;Do not provide financial advice. If the user asks for investment recommendations, reply that you aren't authorized to give financial advice.&amp;quot; Don't write the rule without the fallback response.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Scope deliberately.&lt;/strong&gt; Set Where to Use — Everywhere, CMS only, or Click-to-Chat only — so a rule meant for the public chatbot doesn't reshape the authoring assistant.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Use the trigger when it should be conditional.&lt;/strong&gt; Leave When to Use blank to apply to all in-scope requests, or set a contextual trigger to fire only in specific situations.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Write once, reuse.&lt;/strong&gt; Put tone, brand voice, and cross-cutting policy in instructions rather than repeating them inside each agent prompt.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Remember precedence.&lt;/strong&gt; Liferay's read-only system instructions take priority and can't be overridden by custom instructions — design around them, not against them.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;The precedence rule has another important interpretation. Liferay’s read-only system instructions are the system-level prompts, instructions represent the &lt;strong&gt;&lt;em&gt;your&lt;/em&gt;&lt;/strong&gt; system-level prompts. They combine to constrain the agents from going off the rails regardless of what their individual prompts might be.&lt;/p&gt;





&lt;h2&gt;6. Adjusting guardrails&lt;/h2&gt;





&lt;p&gt;Guardrails run through Google Cloud Model Armor, &lt;strong&gt;outside&lt;/strong&gt; the model; use them for anything that must hold regardless of prompt wording. Instructions ask the model to behave; guardrails enforce.&lt;/p&gt;





&lt;h3&gt;Input guardrails (screen the user message)&lt;/h3&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Malicious URI Filter&lt;/strong&gt; — blocks harmful links.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Prompt Injection and Jailbreak Filter&lt;/strong&gt; — catches attempts to override the agent's instructions.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Sensitive Data Protection&lt;/strong&gt; — scans for and protects PII before it reaches the model.&lt;/li&gt;
&lt;/ul&gt;





&lt;h3&gt;Output guardrails (screen the model reply)&lt;/h3&gt;





&lt;p&gt;Set a Responsible AI threshold (None, Low and Above, Medium and Above, or High) for each of Hate Speech, Dangerous, Sexually Explicit, and Harassment. Higher categories block more aggressively.&lt;/p&gt;





&lt;h3&gt;Recommendations&lt;/h3&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Attach selectively.&lt;/strong&gt; Guardrails apply per-agent and each run adds cost and latency. Put strict input guardrails on public, user-facing agents (chatbots on external sites); lighter rules may suffice for internal CMS authoring agents.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Pair input and output.&lt;/strong&gt; Combine input screening (injection, PII, malicious links) with output screening (harmful/off-policy content) for full coverage.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Tune thresholds to audience.&lt;/strong&gt; Raise output thresholds for public deployments; relax them where over-blocking would frustrate trusted internal users.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Enable multilanguage detection&lt;/strong&gt; when non-English input is expected, so screening isn't limited to English.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Use guardrails, not prompts, for safety.&lt;/strong&gt; Because they sit outside the model, guardrails hold even when a prompt tries to talk its way around the rule. Never rely on prompt wording alone for security-critical limits.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;7. Set External Reference Codes (ERCs) with business meaning&lt;/h2&gt;





&lt;p&gt;Every AI Hub component (agents, chatbots, instructions, and the rest) carries an &lt;strong&gt;External Reference Code (ERC)&lt;/strong&gt;, a unique identifier used for system identification. By default this is an opaque UUID. Even if the ERC isn't something you interact with much today, set it to a value that carries business meaning rather than leaving the generated UUID in place.&lt;/p&gt;





&lt;p&gt;For an instruction that governs response formatting, for example, prefer an ERC like &lt;code&gt;&lt;em&gt;&amp;lt;PROJECT&amp;gt;&lt;/em&gt;-RESPONSE-FORMATTING&lt;/code&gt; over a random UUID. The convention is cheap to adopt and pays off in several ways:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Readable and stable.&lt;/strong&gt; A meaningful ERC is a durable, human-legible handle for the component: easier to reference in tickets, runbooks, and conversations than a UUID.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Portable across environments.&lt;/strong&gt; A deterministic, business-meaningful code lets you line up the same logical component across dev, test, and production instead of reconciling different generated IDs.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Future-proofs for headless / API access.&lt;/strong&gt; Plans for headless API access aren't confirmed, but if and when that day comes, a valid, well-chosen ERC is exactly the stable key an API call would target. Setting sensible ERCs now puts you ahead of the game rather than retrofitting them later.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt; adopt a simple, consistent ERC naming convention early, e.g. &lt;code&gt;&lt;em&gt;&amp;lt;PROJECT&amp;gt;&lt;/em&gt;-&lt;em&gt;&amp;lt;COMPONENT-TYPE&amp;gt;&lt;/em&gt;-&lt;em&gt;&amp;lt;PURPOSE&amp;gt;&lt;/em&gt;&lt;/code&gt;, and apply it to every agent, chatbot, and instruction as you create them, while the set is still small.&lt;/p&gt;





&lt;h2&gt;8. One-page cheat sheet&lt;/h2&gt;





&lt;table style="width: 100.0%;"&gt;
	&lt;thead&gt;
		&lt;tr&gt;
			&lt;th
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;text-align: left;width: 45.0%;"&gt;Goal&lt;/th&gt;
			&lt;th
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;text-align: left;"&gt;Do this&lt;/th&gt;
		&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;New capability / discrete task&lt;/td&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Build a custom agent (duplicate a built-in, edit its workflow, use meaningful name and description for proper selection)&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Same rule across many agents&lt;/td&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Write an instruction (rule + response + scope)&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Must-hold safety limit&lt;/td&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Add a guardrail (input and/or output)&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Ground answers in real content&lt;/td&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Attach a data source and Sync Now&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Several user-visible variants&lt;/td&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Separate focused agents; let the supervisor route&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Hidden internal branching&lt;/td&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;One agent, forking workflow&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Deploy conversation to a page/site&lt;/td&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Chatbot with a curated set of Assigned Agents&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Writing help inside the editor&lt;/td&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;The CMS AI Assistant (built-in + custom agents)&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Identify a component&lt;/td&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Set a business-meaningful ERC (e.g. &lt;code&gt;&lt;em&gt;&amp;lt;PROJECT&amp;gt;&lt;/em&gt;-RESPONSE-FORMATTING&lt;/code&gt;), not a UUID&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td
        style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Watch spend&lt;/td&gt;
			&lt;td style="border: 1.0px solid rgb(208,208,208);padding: 8.0px 12.0px;"&gt;Track Liferay Tokens (LRT) balance on the Activity page&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;





&lt;h1&gt;Conclusion&lt;/h1&gt;





&lt;p&gt;Hopefully these tips will help you make the most of your AI Hub agent building experience!&lt;/p&gt;





&lt;p&gt;Leave a comment below if you have suggestions and/or questions.&lt;/p&gt;</summary>
    <dc:creator>David H Nebinger</dc:creator>
    <dc:date>2026-07-22T15:14:00Z</dc:date>
  </entry>
  <entry>
    <title>KDXP: One Kotlin Codebase for Liferay Apps Everywhere</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126323612" />
    <author>
      <name>Nestor Ledon</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126323612</id>
    <updated>2026-07-16T01:28:45Z</updated>
    <published>2026-07-15T16:58:00Z</published>
    <summary type="html">&lt;p data-end="384" data-start="196"&gt;Liferay already provides an impressive enterprise backend: authentication, users, roles, sites, documents, content, custom Objects, permissions, workflows, and headless APIs.&lt;/p&gt;





&lt;p data-end="497" data-start="386"&gt;What it has lacked is a modern, unified client framework for building applications across every major platform.&lt;/p&gt;





&lt;p data-end="528" data-start="499"&gt;That is the goal of &lt;strong&gt;KDXP&lt;/strong&gt;.&lt;/p&gt;





&lt;p data-end="712" data-start="530"&gt;KDXP is a Kotlin Multiplatform and Compose Multiplatform SDK that allows developers to build Android, iOS, Desktop JVM, and Web applications from one shared codebase.&lt;/p&gt;





&lt;p data-end="743" data-start="714"&gt;Current capabilities include:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;OAuth 2.0 Authorization Code + PKCE&lt;/li&gt;
	&lt;li&gt;Site discovery and switching&lt;/li&gt;
	&lt;li&gt;Liferay Objects&lt;/li&gt;
	&lt;li&gt;Documents and users&lt;/li&gt;
	&lt;li&gt;Manifest-driven branding and navigation&lt;/li&gt;
	&lt;li&gt;Role-aware navigation&lt;/li&gt;
	&lt;li&gt;Adaptive mobile, tablet, and desktop layouts&lt;/li&gt;
	&lt;li&gt;Site-aware deep links&lt;/li&gt;
&lt;/ul&gt;





&lt;p data-end="1023" data-start="992"&gt;The long-term vision is simple:&lt;/p&gt;





&lt;blockquote&gt;
&lt;p data-end="1079" data-start="1027"&gt;&lt;strong&gt;One codebase. All platforms. Powered by Liferay.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;





&lt;p data-end="1242" data-start="1081"&gt;KDXP is currently in active development, and I am looking to connect with Liferay partners and enterprise teams interested in pilots, feedback, or collaboration.&lt;/p&gt;





&lt;p data-end="1374"
    data-start="1244"&gt;&lt;strong&gt;Learn more:&lt;/strong&gt;&lt;br&gt;
&lt;a
    data-end="1374" data-start="1262"
    href="https://www.linkedin.com/products/ledon-augmented-intelligence-kdxp-for-liferay/"
    rel="noopener" target="_new"&gt;KDXP for Liferay on LinkedIn&lt;/a&gt;&lt;/p&gt;





&lt;p data-end="1436" data-is-last-node="" data-is-only-node=""
    data-start="1376"&gt;&lt;strong&gt;Contact:&lt;/strong&gt;&lt;br&gt;
&lt;a data-end="1436"
    data-is-last-node="" data-start="1391"
    href="https://nestorledon.carrd.co/" rel="noopener" target="_new"&gt;Nestor Ledon&lt;/a&gt;&lt;/p&gt;</summary>
    <dc:creator>Nestor Ledon</dc:creator>
    <dc:date>2026-07-15T16:58:00Z</dc:date>
  </entry>
  <entry>
    <title>No More Guesswork: Deploy Your Liferay OSGi Modules in the Right Order, Automatically</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126295475" />
    <author>
      <name>Ankit Hadiyal</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126295475</id>
    <updated>2026-07-06T22:06:22Z</updated>
    <published>2026-07-06T17:40:00Z</published>
    <summary type="html">&lt;h2&gt;A Problem Every Liferay Developer Has Faced&lt;/h2&gt;





&lt;p data-sourcepos="11:1-11:112;356-467"&gt;If you have worked on a Liferay DXP project with more than a few custom modules, this scene will feel familiar.&lt;/p&gt;





&lt;p data-sourcepos="13:1-13:180;469-648"&gt;You deploy all your modules together. Most of them start up without any trouble. But a few fail, and when you open the logs, you find a bundle that stopped right after it started.&lt;/p&gt;





&lt;p data-sourcepos="15:1-15:307;650-956"&gt;The reason is almost always the same. One module tries to start before the module it depends on is even ready. Say your service module depends on your API module, and both get deployed at the same time. The service module may try to start first, find that the API module is not active yet, and simply fail.&lt;/p&gt;





&lt;p data-sourcepos="17:1-17:204;958-1161"&gt;The usual fix is to stop, work out the correct order by hand, and deploy the modules one at a time. It gets the job done, but it takes time, and it is a step you end up repeating with almost every build.&lt;/p&gt;





&lt;h2&gt;The Solution: A Plugin That Handles the Order For You&lt;/h2&gt;





&lt;p data-sourcepos="21:1-21:115;1221-1335"&gt;To take this manual work off your plate, we built a Gradle plugin called &lt;strong&gt;&lt;code&gt;io.github.ankitt-29.deploy-ordered&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;





&lt;p data-sourcepos="23:1-23:374;1337-1710"&gt;In simple terms, the plugin looks at your entire Liferay workspace, works out which module depends on which, and deploys everything in the correct order on its own. It does not stop at deploying and hoping for the best, either. It connects to Liferay's Gogo shell and confirms that every module has actually started successfully before moving on to the next set of modules.&lt;/p&gt;





&lt;p data-sourcepos="25:1-25:164;1712-1875"&gt;The result: no manual restarts, no guessing which module should go first, and no repeated deploy attempts. Just one command, and the plugin takes care of the rest.&lt;/p&gt;





&lt;h2&gt;How It Works, In Plain Words&lt;/h2&gt;





&lt;p data-sourcepos="29:1-29:62;1910-1971"&gt;Here is the simple version of what happens behind the scenes:&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;&lt;strong&gt;It scans your workspace&lt;/strong&gt; and finds every OSGi module you have.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;It builds a dependency map&lt;/strong&gt;, so it knows exactly which module needs which other module to be ready first.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;It groups modules into waves.&lt;/strong&gt; Modules with no dependencies go into Wave 1. Modules that depend only on Wave 1 modules go into Wave 2. This continues until every module has its place.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;It deploys one wave at a time.&lt;/strong&gt; The plugin drops a wave's modules into your Liferay deploy folder, waits a short while, and then checks the Gogo shell to confirm each one has reached the &lt;code&gt;Active&lt;/code&gt; state (or &lt;code&gt;Resolved&lt;/code&gt;, for fragment bundles). Only once a wave is confirmed healthy does it move on to the next one.&lt;/li&gt;
&lt;/ol&gt;





&lt;p data-sourcepos="36:1-36:85;2663-2747"&gt;By the time any module starts, everything it depends on is already active and ready.&lt;/p&gt;





&lt;h2&gt;Before You Begin: Two Things to Set Up&lt;/h2&gt;





&lt;p data-sourcepos="40:1-40:44;2792-2835"&gt;Getting started needs just two small steps.&lt;/p&gt;





&lt;h3&gt;1. Enable the Gogo Shell&lt;/h3&gt;





&lt;p data-sourcepos="44:1-44:155;2867-3021"&gt;The plugin uses Liferay's Gogo shell to check whether your bundles are active. This shell is switched off by default, so you will need to turn it on once.&lt;/p&gt;





&lt;p data-sourcepos="46:1-46:93;3023-3115"&gt;Open (or create) the &lt;code&gt;portal-ext.properties&lt;/code&gt; file in your Liferay bundle, and add this line:&lt;/p&gt;





&lt;p&gt;properties&lt;/p&gt;





&lt;div class="overflow-auto portlet-msg-info"&gt;module.framework.properties.osgi.console=localhost:11311&lt;/div&gt;





&lt;p data-sourcepos="52:1-52:93;3193-3285"&gt;Restart your Liferay server after saving this file, and the Gogo shell will be ready to use.&lt;/p&gt;





&lt;h3&gt;2. Set Your Liferay Home Path&lt;/h3&gt;





&lt;p data-sourcepos="56:1-56:165;3322-3486"&gt;The plugin needs to know where your Liferay home is, so it can find your deploy folder. Add this path in your &lt;code&gt;gradle.properties&lt;/code&gt; or &lt;code&gt;gradle-local.properties&lt;/code&gt; file:&lt;/p&gt;





&lt;p&gt;properties&lt;/p&gt;





&lt;pre&gt;
&lt;code&gt;liferay.workspace.home.dir=C:/liferay/liferay-dxp&lt;/code&gt;&lt;/pre&gt;





&lt;p data-sourcepos="62:1-62:58;3557-3614"&gt;Use the actual path to your own Liferay home folder here.&lt;/p&gt;





&lt;p data-sourcepos="64:1-64:73;3616-3688"&gt;Once these two things are in place, you are ready to install the plugin.&lt;/p&gt;





&lt;h2&gt;Installing the Plugin&lt;/h2&gt;





&lt;p data-sourcepos="68:1-68:80;3716-3795"&gt;Add this small block to the root &lt;code&gt;build.gradle&lt;/code&gt; file of your Liferay workspace:&lt;/p&gt;





&lt;p&gt;groovy&lt;/p&gt;





&lt;div class="overflow-auto portlet-msg-info"&gt;plugins {&lt;br&gt;
    id 'io.github.ankitt-29.deploy-ordered' version '1.0.2'&lt;br&gt;
}&lt;/div&gt;





&lt;p data-sourcepos="76:1-76:47;3884-3930"&gt;That's it. There is nothing else to configure.&lt;/p&gt;





&lt;h2&gt;Running It&lt;/h2&gt;





&lt;p data-sourcepos="80:1-80:86;3947-4032"&gt;With the plugin installed, deploying your modules correctly is a single command away:&lt;/p&gt;





&lt;p&gt;bash&lt;/p&gt;





&lt;div class="overflow-auto portlet-msg-info"&gt;./gradlew deployOrdered&lt;/div&gt;





&lt;p data-sourcepos="86:1-86:150;4071-4220"&gt;Run this, and the plugin will scan, group, deploy, and verify every module for you, one wave at a time, until your whole workspace is up and running.&lt;/p&gt;





&lt;h2&gt;Before You Go&lt;/h2&gt;





&lt;p data-sourcepos="90:1-90:187;4240-4426"&gt;This is the first release of the plugin, version 1.0.2, and it is built with one goal in mind: to save Liferay developers the time and frustration that comes from manual module ordering.&lt;/p&gt;





&lt;p data-sourcepos="92:1-92:248;4428-4675"&gt;It is open for the whole Liferay community to use, and being an early release, there is always room to make it better. If you try it out and have an idea, run into an issue, or think of a feature that would make it more useful, please do share it:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a
      href="https://github.com/ankitt-29/liferay-ordered-deploy"&gt;https://github.com/ankitt-29/liferay-ordered-deploy&lt;/a&gt;&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Gradle Plugin Portal:&lt;/strong&gt; &lt;a href="https://plugins.gradle.org/plugin/io.github.ankitt-29.deploy-ordered"&gt;https://plugins.gradle.org/plugin/io.github.ankitt-29.deploy-ordered&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;





&lt;p data-sourcepos="97:1-97:119;4841-4959"&gt;Every suggestion helps make this a better tool for the whole community. Give it a try, and let us know what you think!&lt;/p&gt;</summary>
    <dc:creator>Ankit Hadiyal</dc:creator>
    <dc:date>2026-07-06T17:40:00Z</dc:date>
  </entry>
  <entry>
    <title>Beyond GET: Could HTTP QUERY (RFC 10008) Improve Liferay Headless APIs?</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126213193" />
    <author>
      <name>Laxit Khanpara</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126213193</id>
    <updated>2026-07-06T22:16:39Z</updated>
    <published>2026-07-03T09:27:00Z</published>
    <summary type="html">&lt;h2&gt;Introduction&lt;/h2&gt;





&lt;p&gt;One of the biggest strengths of Liferay DXP is its Headless API ecosystem. Whether we're working with Custom Objects, Web Content, Documents, Commerce, or custom REST Builder APIs, Liferay provides well-designed REST endpoints that are easy to integrate with modern frontend applications.&lt;/p&gt;





&lt;p&gt;For simple data retrieval, the existing APIs work extremely well.&lt;/p&gt;





&lt;p&gt;For example:&lt;/p&gt;





&lt;pre&gt;
GET /o/c/employees&lt;/pre&gt;





&lt;p&gt;Filtering is also straightforward:&lt;/p&gt;





&lt;pre&gt;
GET /o/c/employees?filter=department eq 'IT'&lt;/pre&gt;





&lt;p&gt;&lt;br&gt;
However, as applications become more sophisticated, search requirements become significantly more complex. Dynamic filters, nested conditions, relationship queries, multiple sorting options, pagination, field selection, and AI-generated search requests can quickly turn a clean URL into something difficult to read and maintain.&lt;/p&gt;





&lt;p&gt;Recently I came across &lt;strong&gt;RFC 10008,&lt;/strong&gt; which introduces a new HTTP method named QUERY.&lt;/p&gt;





&lt;p&gt;Although the RFC is still new and not yet widely supported by frameworks such as Spring Boot or Jakarta REST, it immediately made me think about Liferay Headless APIs and how this method could improve the developer experience in the future.&lt;/p&gt;





&lt;p&gt;This article is not a proposal to replace existing APIs, but rather an exploration of how HTTP QUERY could fit naturally into Liferay's Headless ecosystem.&lt;/p&gt;





&lt;p&gt;The Current Experience&lt;br&gt;
Suppose we have a Liferay Object called Employee.&lt;/p&gt;





&lt;p&gt;Today we retrieve records like this:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;GET /o/c/employees&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;&lt;br&gt;
If we want filtering:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;GET /o/c/employees?filter=(department eq 'IT' and salary gt 50000)&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;&lt;br&gt;
Sorting:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;GET /o/c/employees?sort=name:asc&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;&lt;br&gt;
Pagination:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;GET /o/c/employees?page=1&amp;amp;pageSize=20&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;&lt;br&gt;
Selecting fields:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;GET /o/c/employees?fields=id,name,email&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;&lt;br&gt;
Liferay already provides excellent support for these features.&lt;/p&gt;





&lt;p&gt;The challenge appears when all of these capabilities need to be combined into a single request.&lt;/p&gt;





&lt;p&gt;A real-world URL may eventually contain:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;cite&gt;complex filter expressions&lt;/cite&gt;&lt;/li&gt;
	&lt;li&gt;&lt;cite&gt;nested logical operators&lt;/cite&gt;&lt;/li&gt;
	&lt;li&gt;&lt;cite&gt;relationship filters&lt;/cite&gt;&lt;/li&gt;
	&lt;li&gt;&lt;cite&gt;multiple sorting fields&lt;/cite&gt;&lt;/li&gt;
	&lt;li&gt;&lt;cite&gt;pagination&lt;/cite&gt;&lt;/li&gt;
	&lt;li&gt;&lt;cite&gt;selected fields&lt;/cite&gt;&lt;/li&gt;
	&lt;li&gt;&lt;cite&gt;nested fields&lt;/cite&gt;&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;The URL becomes increasingly difficult to understand.&lt;/p&gt;





&lt;p&gt;Why GET Starts Showing Its Limits&lt;br&gt;
GET requests were designed around URL parameters.&lt;/p&gt;





&lt;p&gt;They work perfectly for small amounts of information.&lt;/p&gt;





&lt;p&gt;But enterprise applications often require much richer search criteria.&lt;/p&gt;





&lt;p&gt;Consider a dynamic search screen where users can create rules such as:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;Department = IT&lt;/li&gt;
	&lt;li&gt;Salary &amp;gt; 50,000&lt;/li&gt;
	&lt;li&gt;Experience &amp;gt;= 5 years&lt;/li&gt;
	&lt;li&gt;Location = India&lt;/li&gt;
	&lt;li&gt;Status = Active&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;The frontend eventually has to generate a filter string similar to:&lt;/p&gt;





&lt;pre&gt;
(department eq 'IT' and salary gt 50000 and experience ge 5 and location eq 'India' and status eq 'Active')
Now imagine adding multiple OR groups, relationships, date ranges, and custom object fields.&lt;/pre&gt;





&lt;p&gt;The filter quickly becomes difficult to read, difficult to debug, and difficult to generate programmatically.&lt;/p&gt;





&lt;p&gt;The Common Workaround&lt;br&gt;
Many REST APIs solve this problem by creating a search endpoint using POST.&lt;/p&gt;





&lt;p&gt;For example:&lt;/p&gt;





&lt;pre&gt;
POST /employees/search
{
  &amp;quot;department&amp;quot;: &amp;quot;IT&amp;quot;,
  &amp;quot;salary&amp;quot;: {
    &amp;quot;gt&amp;quot;: 50000
  },
  &amp;quot;experience&amp;quot;: {
    &amp;quot;gte&amp;quot;: 5
  }
}
Technically this works very well.&lt;/pre&gt;





&lt;ol&gt;
	&lt;li&gt;However, semantically, POST usually represents an operation that creates or processes resources.&lt;/li&gt;
	&lt;li&gt;Searching does neither.&lt;/li&gt;
	&lt;li&gt;We're simply retrieving data.&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;This is exactly the problem that RFC 10008 attempts to solve.&lt;/p&gt;





&lt;h2&gt;What is HTTP QUERY?&lt;br&gt;
RFC 10008 introduces a new HTTP method called QUERY.&lt;/h2&gt;





&lt;p&gt;Unlike GET, QUERY allows a request body while still representing a safe, read-only operation.&lt;/p&gt;





&lt;p&gt;For example:&lt;/p&gt;





&lt;pre&gt;
QUERY /o/c/employees
Content-Type: application/json
{
  &amp;quot;department&amp;quot;: &amp;quot;IT&amp;quot;,
  &amp;quot;salary&amp;quot;: {
    &amp;quot;gt&amp;quot;: 50000
  }
}&lt;/pre&gt;





&lt;p&gt; &lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;Nothing is created.&lt;/li&gt;
	&lt;li&gt;Nothing is updated.&lt;/li&gt;
	&lt;li&gt;Nothing is deleted.&lt;/li&gt;
	&lt;li&gt;The request simply asks the server to return matching data.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;Conceptually, this feels like a very natural fit for search operations.&lt;/p&gt;





&lt;h3&gt;How Could This Help Liferay?&lt;/h3&gt;





&lt;p&gt; &lt;/p&gt;





&lt;h4&gt;Structured Search Requests&lt;/h4&gt;





&lt;p&gt;Instead of encoding complex logic inside URLs:&lt;/p&gt;





&lt;pre&gt;
GET /o/c/employees?filter=(department eq 'IT' and salary gt 50000)
Developers could send structured JSON.

{
  &amp;quot;filter&amp;quot;: {
    &amp;quot;department&amp;quot;: &amp;quot;IT&amp;quot;,
    &amp;quot;salary&amp;quot;: {
      &amp;quot;gt&amp;quot;: 50000
    }
  },
  &amp;quot;sort&amp;quot;: [
    {
      &amp;quot;field&amp;quot;: &amp;quot;name&amp;quot;,
      &amp;quot;direction&amp;quot;: &amp;quot;ASC&amp;quot;
    }
  ],
  &amp;quot;page&amp;quot;: 1,
  &amp;quot;pageSize&amp;quot;: 20
}
JSON is easier to generate, validate, and debug than long query strings.&lt;/pre&gt;





&lt;h4&gt;2. Better Support for Dynamic Liferay Objects&lt;/h4&gt;





&lt;p&gt;One of Liferay's greatest strengths is that administrators can create Objects without writing code.&lt;/p&gt;





&lt;p&gt;Every environment may have different:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;object definitions&lt;/li&gt;
	&lt;li&gt;custom fields&lt;/li&gt;
	&lt;li&gt;relationships&lt;/li&gt;
	&lt;li&gt;picklists&lt;/li&gt;
	&lt;li&gt;validations&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;As these object models become increasingly dynamic, representing search criteria as structured JSON becomes much more natural than constructing long filter expressions.&lt;/p&gt;





&lt;h4&gt;3. Simpler Relationship Queries&lt;/h4&gt;





&lt;p&gt;Today relationship filtering often relies on filter expressions.&lt;/p&gt;





&lt;p&gt;Imagine instead writing:&lt;/p&gt;





&lt;pre&gt;
{
  &amp;quot;department&amp;quot;: {
    &amp;quot;name&amp;quot;: &amp;quot;Engineering&amp;quot;
  }
}&lt;/pre&gt;





&lt;p&gt;&lt;br&gt;
This structure is easier for developers to understand and aligns more closely with how object relationships are represented.&lt;/p&gt;





&lt;h4&gt;4. Better Low-Code Experience&lt;/h4&gt;





&lt;ul&gt;
	&lt;li&gt;Liferay promotes low-code application development.&lt;/li&gt;
	&lt;li&gt;Visual filter builders usually generate JSON internally.&lt;/li&gt;
&lt;/ul&gt;





&lt;pre&gt;
Today the flow often looks like this:
UI
 ↓
JSON
 ↓
Convert to filter string
 ↓
GET request
--------------------------------------------------------------------
With QUERY the process could become:
UI
 ↓
JSON
 ↓
QUERY request&lt;/pre&gt;





&lt;p&gt;&lt;br&gt;
The conversion layer disappears.&lt;/p&gt;





&lt;h4&gt;5. Better AI Integration&lt;/h4&gt;





&lt;ul&gt;
	&lt;li&gt;As AI becomes part of enterprise applications, structured APIs become increasingly valuable.&lt;/li&gt;
	&lt;li&gt;Large Language Models naturally generate JSON.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;Generating:&lt;/p&gt;





&lt;pre&gt;
{
  &amp;quot;department&amp;quot;: &amp;quot;IT&amp;quot;,
  &amp;quot;salary&amp;quot;: {
    &amp;quot;gt&amp;quot;: 50000
  }
}&lt;/pre&gt;





&lt;p&gt; &lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;is generally simpler than generating complex filter syntax.&lt;/li&gt;
	&lt;li&gt;For future AI-powered Liferay applications, structured query bodies could provide a cleaner integration model.&lt;/li&gt;
&lt;/ul&gt;





&lt;h4&gt;6. Easier Validation&lt;/h4&gt;





&lt;ul&gt;
	&lt;li&gt;A structured request body can be validated using existing JSON validation techniques.&lt;/li&gt;
	&lt;li&gt;Instead of parsing long filter strings, APIs could validate request objects before executing searches.&lt;/li&gt;
	&lt;li&gt;This may simplify both server-side validation and client-side development.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;If HTTP QUERY eventually gains widespread adoption across browsers, frameworks, API gateways, and OpenAPI tooling, it could become an interesting addition to Liferay's Headless platform.&lt;/p&gt;





&lt;p&gt;Developers could continue using GET for simple retrieval while choosing QUERY whenever complex, structured search criteria are required.&lt;/p&gt;





&lt;p&gt;Final Thoughts&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;RFC 10008 is still relatively new, and widespread adoption will likely take time.&lt;/li&gt;
	&lt;li&gt;However, reading the specification made me think about the future of Liferay Headless APIs.&lt;/li&gt;
	&lt;li&gt;As Liferay continues investing in Headless delivery, Custom Objects, low-code development, and AI-powered applications, the ability to express rich, structured, read-only searches through HTTP QUERY feels like a natural evolution.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;Perhaps this isn't something Liferay should implement today.&lt;/p&gt;





&lt;p&gt;But it is certainly an interesting conversation for the community.&lt;/p&gt;





&lt;p&gt;I'd love to hear what other Liferay developers think.&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Would HTTP QUERY make your Headless APIs easier to consume?&lt;/li&gt;
	&lt;li&gt;Would you use it for Custom Objects?&lt;/li&gt;
	&lt;li&gt;Do you see value in structured search requests over long filter expressions?&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;Let's discuss.&lt;/p&gt;</summary>
    <dc:creator>Laxit Khanpara</dc:creator>
    <dc:date>2026-07-03T09:27:00Z</dc:date>
  </entry>
  <entry>
    <title>Smooth Sailing: Migrating Liferay DXP from 7.2 to 7.4 (2026 Q1)</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126136896" />
    <author>
      <name>Madhukar Kumar</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126136896</id>
    <updated>2026-06-20T22:26:10Z</updated>
    <published>2026-06-20T12:47:00Z</published>
    <summary type="html">&lt;p&gt;If you are planning this upgrade, here is your definitive, step-by-step technical playbook to take your local environment, database, and codebase from 7.2 to 7.4.&lt;/p&gt;





&lt;h2&gt;&lt;strong&gt;Phase 1: Environment &amp;amp; Local 7.2 Baseline Setup&lt;/strong&gt;&lt;/h2&gt;





&lt;p&gt;Before jumping into the new version, you must recreate a stable, working baseline of your Liferay 7.2 environment locally.&lt;/p&gt;





&lt;h3&gt;&lt;strong&gt;1. Upgrade the Core Runtime&lt;/strong&gt;&lt;/h3&gt;





&lt;p&gt;Liferay 7.4 (2026 Q1) demands modern Java infrastructure.&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Task:&lt;/strong&gt; Upgrade your Java development kit from &lt;strong&gt;JDK 8&lt;/strong&gt; to &lt;strong&gt;JDK 21&lt;/strong&gt;.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Why?&lt;/strong&gt; JDK 21 provides the long-term support (LTS) foundation required by modern Liferay bundles.&lt;/li&gt;
&lt;/ul&gt;





&lt;h3&gt;&lt;strong&gt;2. Restore the Local 7.2 Environment&lt;/strong&gt;&lt;/h3&gt;





&lt;p&gt;To ensure a reliable migration, test the upgrade against a real data snapshot.&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Database Dump:&lt;/strong&gt; Download a fresh database dump from your Development environment.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Import Data:&lt;/strong&gt; Restore this dump into your local MySQL instance.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Data Folder:&lt;/strong&gt; Unzip and sync the data/ folder (which holds your document library and file store) from your 7.2 backup.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Initialize 7.2 Workspace:&lt;/strong&gt; Download and unzip your matching Liferay 7.2.0 bundle, connect it to your imported database, and boot it up once to ensure everything resolves cleanly.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;&lt;strong&gt;Phase 2: Preparing the Target 7.4 Environment&lt;/strong&gt;&lt;/h2&gt;





&lt;p&gt;With the baseline established, it’s time to build the target framework.&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;&lt;strong&gt;Download the Target Bundle:&lt;/strong&gt; Obtain the official &lt;strong&gt;Liferay DXP 7.4 2026Q1&lt;/strong&gt; bundle.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Unzip the Bundle:&lt;/strong&gt; Extract it to your desired local directory (this will be your new $LIFERAY_HOME).&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Initial Data Sync:&lt;/strong&gt; Copy your existing data/ directory from the old 7.2 Tomcat installation directly into the new 7.4 Tomcat directory.&lt;/li&gt;
&lt;/ol&gt;





&lt;h2&gt;&lt;strong&gt;Phase 3: Database Upgrade (MySQL 5.7 to 8.*)&lt;/strong&gt;&lt;/h2&gt;





&lt;p&gt;Liferay 7.4 thrives on MySQL 8.*. Upgrading your database topology safely requires a strict sequence:&lt;/p&gt;





&lt;h3&gt;&lt;strong&gt;Step 1: Pre-Upgrade Compatibility Checks&lt;/strong&gt;&lt;/h3&gt;





&lt;p&gt;Before moving any data, use the MySQL Shell utility to ensure your 5.7 database won’t conflict with 8.* syntax or features. Run the following command:&lt;/p&gt;





&lt;p&gt;Bash&lt;/p&gt;





&lt;p&gt;mysqlsh –user=root –password –host=localhost –util checkForServerUpgrade –targetVersion=8.0.35 –outputFormat=TEXT&lt;/p&gt;





&lt;h3&gt;&lt;strong&gt;Step 2: Backup and Migrate&lt;/strong&gt;&lt;/h3&gt;





&lt;ol&gt;
	&lt;li&gt;&lt;strong&gt;Create a Full Data Backup:&lt;/strong&gt; Export a comprehensive .sql backup of your checked 5.7 database.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Spin up MySQL 8.*:&lt;/strong&gt; Create a brand new MySQL 8.* instance (or Docker container).&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Crucial Configuration:&lt;/strong&gt; Ensure lower_case_table_names=1 is configured in your my.cnf or set as a flag when starting your MySQL 8.0 container.&lt;br&gt;
	⚠️ &lt;strong&gt;Warning:&lt;/strong&gt; Failing to set lower_case_table_names=1 before database initialization can cause irreversible Liferay database schema mismatches on certain operating systems.&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Import Data:&lt;/strong&gt; Use DBeaver (or your preferred database client) to import your backup SQL directly into the new MySQL 8.* instance.&lt;/li&gt;
&lt;/ol&gt;





&lt;h2&gt;&lt;strong&gt;Phase 4: Executing the DB Upgrade Client&lt;/strong&gt;&lt;/h2&gt;





&lt;p&gt;Instead of upgrading on the fly, use Liferay’s dedicated DB Upgrade Client tool for a controlled, visible schema migration.&lt;/p&gt;





&lt;h3&gt;&lt;strong&gt;1. Configure the Upgrade Properties&lt;/strong&gt;&lt;/h3&gt;





&lt;p&gt;Navigate to: \liferay-dxp\tools\portal-tools-db-upgrade-client\ Create or edit the portal-upgrade-ext.properties file to point directly to your new MySQL 8.* instance:&lt;/p&gt;





&lt;p&gt;Properties&lt;/p&gt;





&lt;p&gt;liferay.home=../../&lt;/p&gt;





&lt;p&gt;##&lt;/p&gt;





&lt;p&gt;## MySQL Configuration&lt;/p&gt;





&lt;p&gt;##&lt;/p&gt;





&lt;p&gt;jdbc.default.driverClassName=com.mysql.cj.jdbc.Driver&lt;/p&gt;





&lt;p&gt;jdbc.default.url=jdbc:mysql://localhost:3306/lportal?useSSL=false&amp;amp;characterEncoding=UTF-8&amp;amp;dontTrackOpenResources=true&amp;amp;holdResultsOpenOverStatementClose=true&amp;amp;serverTimezone=GMT&amp;amp;useFastDateParsing=false&amp;amp;useUnicode=true&lt;/p&gt;





&lt;p&gt;jdbc.default.username=****&lt;/p&gt;





&lt;p&gt;jdbc.default.password=****&lt;/p&gt;





&lt;h3&gt;&lt;strong&gt;2. Run the Client&lt;/strong&gt;&lt;/h3&gt;





&lt;p&gt;Open your terminal, navigate to the tool folder, and execute the upgrade script:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;strong&gt;Windows:&lt;/strong&gt; db_upgrade_client.bat&lt;/li&gt;
	&lt;li&gt;&lt;strong&gt;Mac/Linux:&lt;/strong&gt; ./db_upgrade_client.sh&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;Monitor the console logs closely for any schema errors or breaking changes.&lt;/p&gt;





&lt;h2&gt;&lt;strong&gt;Phase 5: Core Upgrade &amp;amp; Target Startup&lt;/strong&gt;&lt;/h2&gt;





&lt;p&gt;Once the database schema is officially upgraded, prepare your 7.4 server properties for its maiden launch.&lt;/p&gt;





&lt;h3&gt;&lt;strong&gt;1. Environment Configurations&lt;/strong&gt;&lt;/h3&gt;





&lt;p&gt;Copy over your key portal configuration files into the new environment, ensuring you update the paths, schemas, and credentials to reflect the 7.4 layout:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;portal-ext.properties&lt;/li&gt;
	&lt;li&gt;portal-setup-wizard.properties&lt;/li&gt;
&lt;/ul&gt;





&lt;h3&gt;&lt;strong&gt;2. Force Auto-Run Check&lt;/strong&gt;&lt;/h3&gt;





&lt;p&gt;Open your active portal-ext.properties file and append this property to ensure missing tables or data structures auto-repair on boot:&lt;/p&gt;





&lt;p&gt;Properties&lt;/p&gt;





&lt;p&gt;upgrade.database.auto.run=true&lt;/p&gt;





&lt;h3&gt;&lt;strong&gt;3. Clean Licenses &amp;amp; Prep Activation&lt;/strong&gt;&lt;/h3&gt;





&lt;ul&gt;
	&lt;li&gt;Navigate to your synced data/license/ folder and &lt;strong&gt;remove&lt;/strong&gt; all old .li licenses.&lt;/li&gt;
	&lt;li&gt;Deploy your new Liferay DXP 7.4 activation key into the deploy/ folder.&lt;/li&gt;
&lt;/ul&gt;





&lt;h3&gt;&lt;strong&gt;4. Start the Server &amp;amp; Reindex&lt;/strong&gt;&lt;/h3&gt;





&lt;p&gt;Fire up the Liferay 7.4 Tomcat server. Once the server completes its startup sequences cleanly, navigate to the &lt;strong&gt;Control Panel &amp;gt; Search&lt;/strong&gt; and trigger a full &lt;strong&gt;Reindexing&lt;/strong&gt; of all modules to ensure your search indexes match the new data structures.&lt;/p&gt;





&lt;h2&gt;&lt;strong&gt;Phase 6: Code Upgrade&lt;/strong&gt;&lt;/h2&gt;





&lt;p&gt;With the platform up and running, the final step is migrating your custom applications.&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Copy your custom modules and applications from your older 7.2 workspace into the new 7.4 workspace structure.&lt;/li&gt;
	&lt;li&gt;Update your build.gradle dependencies to point to the &lt;strong&gt;7.4 2026Q1&lt;/strong&gt; target platform BOM (Bill of Materials).&lt;/li&gt;
	&lt;li&gt;Address any deprecated APIs or breaking changes within your Java classes and OSGi modules, compile, and deploy!
	&lt;ol&gt;
		&lt;li&gt;Import workspace in intellij&lt;/li&gt;
		&lt;li&gt;Change the workspace target version gradle.properties&lt;/li&gt;
	&lt;/ol&gt;
	&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;liferay.workspace.product=dxp-2026.q1.0&lt;/p&gt;





&lt;p&gt;liferay.workspace.target.platform.version=2026.q1.0&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Set gradle version to latest one in settings.gradle&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;dependencies &lt;strong&gt;{&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt;classpath group: “com.liferay”, name: “com.liferay.gradle.plugins.workspace”, version: “15.0.0”&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;}&lt;/strong&gt;&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Set java version to 21 in Intellij&lt;/li&gt;
&lt;/ol&gt;





&lt;figure&gt;&lt;img alt="" height="739"
    src="https://shreeraminfotech.in/wp-content/uploads/2026/06/image.png" width="988"&gt;&lt;/figure&gt;





&lt;ol&gt;
	&lt;li&gt;String pool&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;            Replace import com.liferay.portal.kernel.util.StringPool;&lt;/p&gt;





&lt;p&gt;            With import com.liferay.petra.string.StringPool;&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Build.gradle&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;             Replace compileOnly group: “com.liferay.portal”, name: “com.liferay.portal.kernel”&lt;/p&gt;





&lt;p&gt;             With compileOnly group: “com.liferay.portal”, name: “release.dxp.api”, version:”default”&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Replace import javax.servlet.http.HttpSession;&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;With import jakarta.servlet.http.HttpSession;&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Replace import com.liferay.portal.kernel.search.generic.MultiMatchQuery;&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;With import com.liferay.portal.kernel.search.MultiMatchQuery;&lt;/p&gt;





&lt;p&gt;Known issue with few modules&lt;/p&gt;





&lt;p&gt; Error: Classes found in the wrong directory:         {META-INF/versions/9/module-info.class=module-info}&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Replace import com.liferay.portal.kernel.captcha.CaptchaUtil&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;            With import com.liferay.portal.kernel.captcha.Captcha;&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Replace ZipWriter zipWriter = ZipWriterFactoryUtil.getZipWriter();&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;With ZipWriter zipWriter = _zipWriterFactory.getZipWriter();&lt;/p&gt;





&lt;p&gt;Add @Reference private ZipWriterFactory _zipWriterFactory;&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Replace compileOnly group: ‘xml-apis’, name: ‘xml-apis’, version: ‘1.0.b.2’&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;With compileOnly group: ‘xml-apis’, name: ‘xml-apis’, version: ‘1.4.01’&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Create a new service builder with the same name or different name using the same service.xml.&lt;/li&gt;
	&lt;li&gt;Implement all finders and impl methods from old service module impl&lt;/li&gt;
	&lt;li&gt;Keep version: “default” in the build.gradle  file.&lt;/li&gt;
	&lt;li&gt;Rerun the build service task and deploy task&lt;/li&gt;
&lt;/ol&gt;





&lt;h2&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h2&gt;





&lt;p&gt;Migrating from Liferay 7.2 to 7.4 might seem daunting, but breaking it down into distinct phases—&lt;strong&gt;Environment Setup, Database Upgrades, Tools Execution, and Code Migration&lt;/strong&gt;—ensures a predictable, successful deployment.&lt;/p&gt;





&lt;p&gt;Take your time with the pre-upgrade checks, keep backups at every phase, and enjoy the massive benefits of Liferay 7.4!&lt;/p&gt;</summary>
    <dc:creator>Madhukar Kumar</dc:creator>
    <dc:date>2026-06-20T12:47:00Z</dc:date>
  </entry>
  <entry>
    <title>CE is sunsetted. What's your next move?</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126120230" />
    <author>
      <name>Ben Turner</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126120230</id>
    <updated>2026-06-19T06:12:02Z</updated>
    <published>2026-06-19T05:36:00Z</published>
    <summary type="html">&lt;p&gt;If you're using Liferay Community Edition (CE), or are an SI that has customers who do, you probably know (well, if you've been anywhere near &lt;em&gt;this&lt;/em&gt; site this year you definitely know!) that the final version of Liferay CE was in early 2025, and that from the 2026.Q1 release of Liferay DXP we've made a Free Tier available to anyone that wants it.&lt;/p&gt;





&lt;p&gt;But, what does this actually mean if you're using CE today? What are your options, and where do you go from here?&lt;/p&gt;





&lt;p&gt;On June 30th I'll be hosting two identical webinars where we'll provide an open and honest view on the three (or four) paths that you could take, and to help you move ahead with confidence.&lt;/p&gt;





&lt;h3&gt;What you'll learn:&lt;/h3&gt;





&lt;ol&gt;
	&lt;li&gt;What actually changed. What the end of Community Edition means in practice for existing CE installations.&lt;/li&gt;
	&lt;li&gt;Your four options. Enterprise, Free Tier, GitHub self-build, or staying on CE — with honest pros and cons for each path.&lt;/li&gt;
	&lt;li&gt;Free Tier in depth. What’s included, what’s not, how activation keys work, and how to get started.&lt;/li&gt;
	&lt;li&gt;When Enterprise makes sense. Clear signals that your project has outgrown the Free Tier — and how straightforward the upgrade process is.&lt;/li&gt;
	&lt;li&gt;Active promotions. Available discounts and migration support through the Core Upgrade Program, including eligibility details.&lt;/li&gt;
	&lt;li&gt;Live Q&amp;amp;A. Ask questions during the session and get direct answers from the Liferay team.&lt;/li&gt;
&lt;/ol&gt;





&lt;h3&gt;When is the webinar?&lt;/h3&gt;





&lt;p&gt;It's taking place on Tuesday, June 30th. There are two identical sessions to cater for as many timezone as possible. Your options are:&lt;/p&gt;





&lt;table border="1" cellpadding="2" cellspacing="2"
        style="width: 100.0%;"&gt;
	&lt;thead&gt;
		&lt;tr&gt;
			&lt;th scope="col"
        style=""&gt;&lt;strong&gt;Session A - EMEA/APAC&lt;/strong&gt;&lt;/th&gt;
			&lt;th
        scope="col" style=""&gt;&lt;strong&gt;Session B - Americas&lt;/strong&gt;&lt;/th&gt;
		&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
		&lt;tr&gt;
			&lt;td&gt;
			&lt;p&gt;09:00 BST - London&lt;/p&gt;
			&lt;/td&gt;
			&lt;td&gt;2:00pm PDT - Los Angeles&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;
			&lt;p&gt;10:00 CEST - Paris/Berlin&lt;/p&gt;
			&lt;/td&gt;
			&lt;td&gt;3:00pm MDT - Denver&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;
			&lt;p&gt;12:00 GST - Dubai&lt;/p&gt;
			&lt;/td&gt;
			&lt;td&gt;4:00pm COT - Bogota&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;
			&lt;p&gt;13:30 IST - Mumbai&lt;/p&gt;
			&lt;/td&gt;
			&lt;td&gt;6:00pm BRT - Sāo Paulo &lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;
			&lt;p&gt;16:00 SGT - Singapore&lt;/p&gt;
			&lt;/td&gt;
			&lt;td&gt;7:00am AEST - Sydney (July 1st)&lt;/td&gt;
		&lt;/tr&gt;
		&lt;tr&gt;
			&lt;td&gt;
			&lt;p&gt;18:00 AEST - Sydney&lt;/p&gt;
			&lt;/td&gt;
			&lt;td&gt;9:00am NZST - Auckland (July 1st)&lt;/td&gt;
		&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;





&lt;p&gt; &lt;/p&gt;





&lt;h3&gt;How do I join?&lt;/h3&gt;





&lt;p&gt;Simple - just visit &lt;a
    href="https://www.liferay.com/en/web/lr/webinar-liferay-dxp-free-tier"
    rel="noopener noreferrer" target="_blank"&gt;here&lt;/a&gt; and register for your preferred session!&lt;/p&gt;





&lt;h3&gt;What if I can't make it?&lt;/h3&gt;





&lt;p&gt;Register anyway - we'll be making a recording available after the session, and if you register but can't attend we'll make a special effort to let you know!&lt;/p&gt;





&lt;p&gt;Cheers - see you all there!&lt;/p&gt;</summary>
    <dc:creator>Ben Turner</dc:creator>
    <dc:date>2026-06-19T05:36:00Z</dc:date>
  </entry>
  <entry>
    <title>Mission-Critical Email Delivery in Liferay</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126045225" />
    <author>
      <name>David H Nebinger</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126045225</id>
    <updated>2026-06-20T22:25:47Z</updated>
    <published>2026-06-17T17:51:00Z</published>
    <summary type="html">&lt;h2&gt;A Simple Question&lt;/h2&gt;





&lt;p&gt;A customer recently reached out after experiencing a six-hour outage of their mail server.&lt;/p&gt;





&lt;p&gt;The outage had nothing to do with Liferay. The platform was healthy. Their application was healthy. Business processes continued to run normally.&lt;/p&gt;





&lt;p&gt;The problem was that some of those business processes generated emails.&lt;/p&gt;





&lt;p&gt;Not marketing emails.&lt;/p&gt;





&lt;p&gt;Not newsletters.&lt;/p&gt;





&lt;p&gt;Mission-critical emails.&lt;/p&gt;





&lt;p&gt;The outage raised an uncomfortable question:&lt;/p&gt;





&lt;p&gt;&lt;em&gt;What happens to those emails when the mail server is unavailable?&lt;/em&gt;&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;They're lost&lt;/strong&gt;, I responded.&lt;/p&gt;





&lt;p&gt;That quickly led to a follow-up question:&lt;/p&gt;





&lt;p&gt;&lt;em&gt;How can I guarantee email delivery from Liferay?&lt;/em&gt;&lt;/p&gt;





&lt;p&gt;And I had to give them an honest answer:&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;You can't.&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt;At least not using Liferay's standard email delivery implementation.&lt;/p&gt;





&lt;p&gt;That answer surprised them, so let's talk about how email delivery works today and why solving the problem requires a different architectural approach.&lt;/p&gt;





&lt;h2&gt;How Liferay Sends Email&lt;/h2&gt;





&lt;p&gt;When a process in Liferay wants to send an email, it typically doesn't connect directly to the SMTP server.&lt;/p&gt;





&lt;p&gt;Instead, Liferay creates a mail message and posts it to the Liferay Message Bus (LMB).&lt;/p&gt;





&lt;p&gt;The LMB is Liferay's internal asynchronous messaging system. It allows work to be handed off from one thread to another, letting the original process continue without waiting for the email to be delivered.&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126045238" src="https://liferay.dev/documents/portlet_file_entry/14/standard-email-flow.png/43fb608a-afeb-e5c4-afe0-2f1b9efe9de9"&gt;&lt;/p&gt;





&lt;p&gt;This approach has several advantages.&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;The user doesn't wait for SMTP communication.&lt;/li&gt;
	&lt;li&gt;Business processes complete more quickly.&lt;/li&gt;
	&lt;li&gt;Email delivery happens asynchronously.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;The important detail, however, is that the LMB is an in-memory messaging system. It is not a durable message broker.&lt;/p&gt;





&lt;p&gt;If the SMTP server is unavailable, the failure is logged, but the message is discarded.&lt;/p&gt;





&lt;p&gt;If Liferay crashes or is restarted while messages remain queued in the LMB, those queued messages are lost.&lt;/p&gt;





&lt;p&gt;For most organizations this is perfectly acceptable. Password reset emails, notifications, subscription updates, and workflow messages can usually tolerate occasional failures.&lt;/p&gt;





&lt;p&gt;Mission-critical messaging often cannot.&lt;/p&gt;





&lt;h2&gt;This Isn't Really a Liferay Problem&lt;/h2&gt;





&lt;p&gt;It's important to recognize that this isn't a flaw in Liferay.&lt;/p&gt;





&lt;p&gt;The platform was designed around immediate email delivery. For the vast majority of deployments, that's exactly the right tradeoff.&lt;/p&gt;





&lt;p&gt;The customer's problem wasn't really about email.&lt;/p&gt;





&lt;p&gt;The customer's problem was about preserving an important business event until it can be processed successfully.&lt;/p&gt;





&lt;p&gt;That's a messaging problem.&lt;/p&gt;





&lt;p&gt;And when someone asks me how to make sure something gets done reliably, I immediately start thinking about durable messaging.&lt;/p&gt;





&lt;h2&gt;Reliable Delivery Starts with JMS&lt;/h2&gt;





&lt;p&gt;When someone asks me how to make sure something gets done reliably, I immediately start thinking about JMS.&lt;/p&gt;





&lt;p&gt;JMS, or Java Message Service, is a standard messaging API used throughout enterprise Java applications. Rather than performing work immediately, applications can place a message onto a &lt;em&gt;durable&lt;/em&gt; queue where it waits until a consumer is ready to process it.&lt;/p&gt;





&lt;p&gt;That distinction is important.&lt;/p&gt;





&lt;p&gt;Instead of asking:&lt;/p&gt;





&lt;p&gt;&lt;em&gt;Can I send this email right now?&lt;/em&gt;&lt;/p&gt;





&lt;p&gt;We can ask:&lt;/p&gt;





&lt;p&gt;&lt;em&gt;Can I preserve the request to send this email until it can be processed successfully?&lt;/em&gt;&lt;/p&gt;





&lt;p&gt;Those are very different questions.&lt;/p&gt;





&lt;p&gt;With JMS, the email itself becomes a message placed onto a queue.&lt;/p&gt;





&lt;p&gt;A separate process consumes messages from that queue and performs the actual work.&lt;/p&gt;





&lt;p&gt;When combined with a durable messaging platform, this provides several advantages:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;The message survives process restarts.&lt;/li&gt;
	&lt;li&gt;The message survives application crashes.&lt;/li&gt;
	&lt;li&gt;The message survives temporary infrastructure failures.&lt;/li&gt;
	&lt;li&gt;The message remains available until it is successfully processed.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;That last point is the important one.&lt;/p&gt;





&lt;p&gt;The queue doesn't care whether the SMTP server is currently available.&lt;/p&gt;





&lt;p&gt;It simply holds the message until a consumer successfully processes it.&lt;/p&gt;





&lt;p&gt;Of course, JMS itself is just an API and a programming model.&lt;/p&gt;





&lt;p&gt;To actually store, manage, and deliver messages, we need a messaging platform behind it.&lt;/p&gt;





&lt;p&gt;That's where an Enterprise Message Bus comes in.&lt;/p&gt;





&lt;h2&gt;Introducing an Enterprise Message Bus&lt;/h2&gt;





&lt;p&gt;For the proof of concept I chose Apache Artemis as the Enterprise Message Bus (EMB).&lt;/p&gt;





&lt;p&gt;Artemis provides durable JMS queues, persistence, retry policies, dead letter queue support, monitoring, and operational tooling.&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126045263" src="https://liferay.dev/documents/portlet_file_entry/14/artemis-queue-empty.png/82cb5b72-9a59-6a93-053e-f3f5d91b8a8b"&gt;&lt;/p&gt;





&lt;div
    class="overflow-auto portlet-msg-info"&gt;Note: If you're using the repo Docker composition, you're going to log into &lt;a
    href="http://localhost:8161" rel="noopener noreferrer"&gt;http://localhost:8161&lt;/a&gt; with username &lt;code&gt;mailuser&lt;/code&gt; and password &lt;code&gt;mailpass&lt;/code&gt;.&lt;/div&gt;





&lt;p&gt;The important point isn't Artemis specifically.&lt;/p&gt;





&lt;p&gt;This same architectural pattern would work with RabbitMQ, ActiveMQ Classic, IBM MQ, Amazon MQ, Azure Service Bus, or any other enterprise messaging platform capable of durable message persistence.&lt;/p&gt;





&lt;p&gt;Artemis simply worked well for the proof of concept and is easy to run locally using Docker.&lt;/p&gt;





&lt;h2&gt;Why Apache Camel?&lt;/h2&gt;





&lt;p&gt;Once the message is sitting in a durable queue, something needs to process it.&lt;/p&gt;





&lt;p&gt;Apache Camel is not required for this solution.&lt;/p&gt;





&lt;p&gt;A custom Java application could consume JMS messages and perform SMTP delivery directly.&lt;/p&gt;





&lt;p&gt;A custom solution could even move template processing out of Liferay entirely and generate emails during message processing.&lt;/p&gt;





&lt;p&gt;There are plenty of possibilities.&lt;/p&gt;





&lt;p&gt;For this proof of concept, however, Apache Camel dramatically reduced the amount of code required.&lt;/p&gt;





&lt;p&gt;One of the reasons I frequently pair an Enterprise Message Bus with Apache Camel is that Camel eliminates a tremendous amount of integration plumbing.&lt;/p&gt;





&lt;p&gt;Without Camel, we'd need to write:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;JMS consumer logic.&lt;/li&gt;
	&lt;li&gt;Transaction management.&lt;/li&gt;
	&lt;li&gt;SMTP integration.&lt;/li&gt;
	&lt;li&gt;Retry handling.&lt;/li&gt;
	&lt;li&gt;Connection management.&lt;/li&gt;
	&lt;li&gt;Error propagation.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;Camel already knows how to consume JMS messages.&lt;/p&gt;





&lt;p&gt;Camel already knows how to work with SMTP.&lt;/p&gt;





&lt;p&gt;Camel already knows how to manage transactions.&lt;/p&gt;





&lt;p&gt;For example, the route for processing the email via camel is a Java class defining the config:&lt;/p&gt;





&lt;pre&gt;
public class MailRelayRoute extends RouteBuilder {
  @Override
  public void configure() {
    from(&amp;quot;sjms2:queue:mail.outbound?connectionFactory&amp;quot; + 
        &amp;quot;=#artemisCF&amp;amp;transacted=true&amp;quot;)
      .routeId(&amp;quot;mail-outbound-relay&amp;quot;)
      .log(&amp;quot;Relaying message ${header.To}&amp;quot;)
      .bean(&amp;quot;smtpSender&amp;quot;, &amp;quot;send&amp;quot;);
  }
}
&lt;/pre&gt;





&lt;p&gt;The resulting route is surprisingly small because Camel is hiding a large amount of infrastructure code we'd otherwise have to write ourselves.&lt;/p&gt;





&lt;p&gt;For me, Enterprise Message Buses and Apache Camel go together like peanut butter and jelly.&lt;/p&gt;





&lt;p&gt;The broker provides durability and reliable delivery.&lt;/p&gt;





&lt;p&gt;Camel provides connectivity and routing.&lt;/p&gt;





&lt;p&gt;Together they allow us to focus on business requirements instead of plumbing.&lt;/p&gt;





&lt;h2&gt;The New Architecture&lt;/h2&gt;





&lt;p&gt;Instead of sending emails directly from the Liferay mail listener, the proof of concept diverts messages into a durable JMS queue.&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126071006" src="https://liferay.dev/documents/portlet_file_entry/14/reliable-email-flow.png/ff5eaacf-5915-11a2-64c2-4a3796aa3db8"&gt;&lt;/p&gt;





&lt;p&gt;The actual customization is surprisingly small.&lt;/p&gt;





&lt;p&gt;A custom listener subscribes to the same mail destination used by Liferay's built-in mail sender.&lt;/p&gt;





&lt;p&gt;When a mail message arrives:&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;Receive the mail message.&lt;/li&gt;
	&lt;li&gt;Serialize it as an RFC822 email.&lt;/li&gt;
	&lt;li&gt;Publish it to a durable JMS queue.&lt;/li&gt;
	&lt;li&gt;Return.&lt;/li&gt;
&lt;/ol&gt;





&lt;p&gt;At that point Liferay's responsibility ends.&lt;/p&gt;





&lt;p&gt;The email request has been durably persisted.&lt;/p&gt;





&lt;p&gt;Everything that happens afterward becomes a messaging concern rather than a Liferay concern.&lt;/p&gt;





&lt;h2&gt;Preventing Duplicate Delivery&lt;/h2&gt;





&lt;p&gt;One obvious question is:&lt;/p&gt;





&lt;p&gt;&lt;em&gt;If Liferay already has a mail sender, won't both listeners process the message?&lt;/em&gt;&lt;/p&gt;





&lt;p&gt;The answer is yes.&lt;/p&gt;





&lt;p&gt;To avoid duplicate delivery, the proof of concept disables the default Liferay mail sender and replaces it with the JMS publisher.&lt;/p&gt;





&lt;p&gt;We create a &lt;code&gt;osgi/configs/com.liferay.portal.component.blacklist.internal.configuration.ComponentBlacklistConfiguration.config&lt;/code&gt; file that contains the following:&lt;/p&gt;





&lt;pre&gt;
blacklistComponentNames=
  [&amp;quot;com.liferay.mail.messaging.internal.MailMessageListener&amp;quot;]
&lt;/pre&gt;





&lt;p&gt;This ensures there is only one path for outbound email processing.&lt;/p&gt;





&lt;p&gt;It blocks the Liferay component from running, so it won't be pulling messages from the LMB, only our customization will.&lt;/p&gt;





&lt;h2&gt;Why RFC822 Serialization Matters&lt;/h2&gt;





&lt;p&gt;Rather than creating a custom message format, the proof of concept serializes the complete email into a standard RFC822 message.&lt;/p&gt;





&lt;p&gt;This preserves:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;HTML content.&lt;/li&gt;
	&lt;li&gt;Attachments.&lt;/li&gt;
	&lt;li&gt;Reply-To addresses.&lt;/li&gt;
	&lt;li&gt;Message headers.&lt;/li&gt;
	&lt;li&gt;Message IDs.&lt;/li&gt;
	&lt;li&gt;Custom metadata.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;Camel doesn't need to understand anything about Liferay mail messages.&lt;/p&gt;





&lt;p&gt;It simply receives a standard email and forwards it appropriately.&lt;/p&gt;





&lt;p&gt;This keeps the integration clean and loosely coupled.&lt;/p&gt;





&lt;h2&gt;Reliable Delivery Through Transactions&lt;/h2&gt;





&lt;p&gt;One small configuration detail carries a lot of weight.&lt;/p&gt;





&lt;p&gt;The Camel route consumes messages transactionally.&lt;/p&gt;





&lt;pre&gt;
from(&amp;quot;sjms2:queue:mail.outbound?connectionFactory=&amp;quot; + 
    &amp;quot;#artemisCF&amp;amp;transacted=true&amp;quot;)
  .routeId(&amp;quot;mail-outbound-relay&amp;quot;)
  .bean(&amp;quot;smtpSender&amp;quot;, &amp;quot;send&amp;quot;);
&lt;/pre&gt;





&lt;p&gt;If SMTP delivery succeeds, the transaction commits and Artemis removes the message from the queue.&lt;/p&gt;





&lt;p&gt;If SMTP delivery fails, the transaction rolls back.&lt;/p&gt;





&lt;p&gt;The message remains in the queue.&lt;/p&gt;





&lt;p&gt;Nothing is lost.&lt;/p&gt;





&lt;p&gt;That transactional boundary is what makes reliable delivery possible.&lt;/p&gt;





&lt;h2&gt;Repository Contents&lt;/h2&gt;





&lt;p&gt;The repository accompanying this post (&lt;a
    href="https://github.com/dnebing/liferay-reliable-email-delivery" rel="noopener noreferrer"&gt;https://github.com/dnebing/liferay-reliable-email-delivery&lt;/a&gt;) contains everything needed to build and run the proof of concept.&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;A Liferay Workspace.&lt;/li&gt;
	&lt;li&gt;The email diversion module.&lt;/li&gt;
	&lt;li&gt;Docker Compose configuration.&lt;/li&gt;
	&lt;li&gt;Apache Artemis.&lt;/li&gt;
	&lt;li&gt;Apache Camel JBang.&lt;/li&gt;
	&lt;li&gt;SMTP4Dev.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;The Liferay Workspace contains the customization module that diverts email messages from the LMB into JMS.&lt;/p&gt;





&lt;p&gt;The Docker composition starts Liferay DXP 2026.Q1.8, Artemis, Camel JBang, and SMTP4Dev.&lt;/p&gt;





&lt;h2&gt;Building and Running the Proof of Concept&lt;/h2&gt;





&lt;p&gt;Build the module from the Liferay Workspace:&lt;/p&gt;





&lt;pre&gt;
$ cd liferay-workspace
$ ./gradlew :modules:guaranteed-email-delivery:jar
&lt;/pre&gt;





&lt;p&gt;Copy the generated module into the Liferay deployment location used by the Docker composition.&lt;/p&gt;





&lt;pre&gt;
$ cp modules/guaranteed-email-delivery/build/libs/*.jar \
  ../docker/liferay/files/osgi/modules/
&lt;/pre&gt;





&lt;p&gt;Start the environment:&lt;/p&gt;





&lt;pre&gt;
$ cd ../docker
$ docker compose up -d
&lt;/pre&gt;





&lt;p&gt;Once the containers are running, verify that:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;Liferay is available on port 8080.&lt;/li&gt;
	&lt;li&gt;Artemis is available on port 8161.&lt;/li&gt;
	&lt;li&gt;SMTP4Dev is available on port 5080.&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;Why SMTP4Dev?&lt;/h2&gt;





&lt;p&gt;For a proof of concept, I didn't want to depend on a real mail server.&lt;/p&gt;





&lt;p&gt;&lt;a href="https://github.com/rnwood/smtp4dev" rel="noopener noreferrer"&gt;SMTP4Dev&lt;/a&gt; is a lightweight SMTP server designed specifically for testing and development.&lt;/p&gt;





&lt;p&gt;It accepts emails, stores them locally, and exposes a web interface for reviewing received messages.&lt;/p&gt;





&lt;p&gt;This gives us several advantages:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;No external mail dependency.&lt;/li&gt;
	&lt;li&gt;No risk of sending emails to real users.&lt;/li&gt;
	&lt;li&gt;Easy inspection of message contents.&lt;/li&gt;
	&lt;li&gt;Simple outage simulation.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;Most importantly, it allows us to reproduce the original customer problem.&lt;/p&gt;





&lt;p&gt;When SMTP4Dev is running, messages should be delivered immediately.&lt;/p&gt;





&lt;p&gt;When SMTP4Dev is stopped, Camel cannot deliver messages, Artemis retains them in the durable queue, and the queue depth increases.&lt;/p&gt;





&lt;p&gt;When SMTP4Dev is started again, Camel resumes processing and the backlog drains.&lt;/p&gt;





&lt;h2&gt;Generating Test Emails&lt;/h2&gt;





&lt;p&gt;To make testing easier, the repository includes a simple Groovy script that can be executed from the Liferay Script Console.&lt;/p&gt;





&lt;p&gt;The script sends an email using Liferay's standard &lt;code&gt;MailServiceUtil&lt;/code&gt; API.&lt;/p&gt;





&lt;pre&gt;
import com.liferay.mail.kernel.model.MailMessage
import com.liferay.mail.kernel.service.MailServiceUtil

import jakarta.mail.internet.InternetAddress

def from = new InternetAddress(&amp;quot;noreply@example.com&amp;quot;,
  &amp;quot;Liferay Reliable Mail Demo&amp;quot;)
def to = new InternetAddress(&amp;quot;test@example.com&amp;quot;, 
  &amp;quot;Test Recipient&amp;quot;)

def subject = &amp;quot;Reliable email delivery test - &amp;quot; + new Date()

def body = &amp;quot;&amp;lt;html&amp;gt;&amp;quot; +
&amp;quot;    &amp;lt;body&amp;gt;&amp;quot; +
&amp;quot;        &amp;lt;h2&amp;gt;Liferay Reliable Email Delivery Test&amp;lt;/h2&amp;gt;&amp;quot; +
&amp;quot;        &amp;lt;p&amp;gt;This email was generated from the Liferay Script &amp;quot;
      + &amp;quot;          Console.&amp;lt;/p&amp;gt;&amp;quot; +
&amp;quot;        &amp;lt;p&amp;gt;&amp;quot; +
&amp;quot;            If SMTP4Dev is running, it should appear &amp;quot; +
&amp;quot;immediately.&amp;lt;br /&amp;gt;&amp;quot; +
&amp;quot;            If SMTP4Dev is stopped, it should remain queued 
        &amp;quot;in Artemis&amp;quot; +
&amp;quot;            until the mail server is available again.&amp;quot; +
&amp;quot;        &amp;lt;/p&amp;gt;&amp;quot; +
&amp;quot;        &amp;lt;p&amp;gt;Generated at: &amp;quot; + new Date().toString() + &amp;quot;&amp;lt;/p&amp;gt;&amp;quot; +
&amp;quot;    &amp;lt;/body&amp;gt;&amp;quot; +
&amp;quot;&amp;lt;/html&amp;gt;&amp;quot;

def mailMessage = new MailMessage(from, to, subject, 
  body, true)

MailServiceUtil.sendEmail(mailMessage)

out.println(&amp;quot;Queued email using Liferay MailServiceUtil: &amp;quot; + 
  subject)
&lt;/pre&gt;





&lt;div class="overflow-auto portlet-msg-info"&gt;NOTE: The script may not copy correctly and paste into the console because it's html. Check the &lt;code&gt;sendmail.groovy&lt;/code&gt; script in the repo, it should work fine.&lt;/div&gt;





&lt;p&gt;Because the script uses the normal Liferay mail infrastructure, it follows exactly the same path as any other email generated by the platform.&lt;/p&gt;





&lt;p&gt;In Liferay, open the Script Console, select Groovy, paste the script, and execute it.&lt;/p&gt;





&lt;h2&gt;Testing the Solution&lt;/h2&gt;





&lt;p&gt;With everything running normally, execute the Groovy script.&lt;/p&gt;





&lt;p&gt;The email should immediately appear in SMTP4Dev.&lt;/p&gt;





&lt;div class="overflow-auto portlet-msg-info"&gt;Note: If using the docker container, go to http://localhost:5080 and you'll see all of the email that has been delivered.&lt;/div&gt;





&lt;p&gt;&lt;img data-fileentryid="126071019" src="https://liferay.dev/documents/portlet_file_entry/14/smtp4dev.png/d3687d83-ede5-4955-9eed-533aeff743a6"&gt;&lt;/p&gt;





&lt;p&gt;Now stop the SMTP server:&lt;/p&gt;





&lt;pre&gt;
$ docker compose stop smtp4dev
&lt;/pre&gt;





&lt;p&gt;Execute the Groovy script several more times.&lt;/p&gt;





&lt;p&gt;At this point:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;Liferay continues generating email requests.&lt;/li&gt;
	&lt;li&gt;Messages are published to Artemis.&lt;/li&gt;
	&lt;li&gt;Camel attempts delivery.&lt;/li&gt;
	&lt;li&gt;SMTP delivery fails.&lt;/li&gt;
	&lt;li&gt;Artemis retains the messages.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;Open the Artemis console and inspect the queue.&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126071030" src="https://liferay.dev/documents/portlet_file_entry/14/5-pending.png/c413fed2-7c92-5733-5187-ae61e97bba49"&gt;&lt;/p&gt;





&lt;p&gt;The queue depth should continue increasing as new messages are generated.&lt;/p&gt;





&lt;p&gt;Nothing is lost.&lt;/p&gt;





&lt;h2&gt;Recovering from the Outage&lt;/h2&gt;





&lt;p&gt;Start SMTP4Dev again:&lt;/p&gt;





&lt;pre&gt;
$ docker compose start smtp4dev
&lt;/pre&gt;





&lt;p&gt;Camel reconnects and resumes processing.&lt;/p&gt;





&lt;p&gt;Messages begin leaving the queue.&lt;/p&gt;





&lt;p&gt;The backlog drains.&lt;/p&gt;





&lt;p&gt;Emails appear in SMTP4Dev.&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126071041" src="https://liferay.dev/documents/portlet_file_entry/14/0-pending.png/71d8c84e-47a5-55e9-9dd4-55b9f074ff61"&gt;&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126071052" src="https://liferay.dev/documents/portlet_file_entry/14/delivered.png/6e6f3edc-11c8-b368-e152-676d2047afa6"&gt;&lt;/p&gt;





&lt;p&gt;This is exactly the behavior we wanted to validate.&lt;/p&gt;





&lt;p&gt;The six-hour mail server outage that prompted the original question no longer results in lost messages.&lt;/p&gt;





&lt;h2&gt;Artemis Retry Configuration&lt;/h2&gt;





&lt;p&gt;The proof of concept includes a retry strategy configured directly in Artemis.&lt;/p&gt;





&lt;pre&gt;
&amp;lt;redelivery-delay&amp;gt;5000&amp;lt;/redelivery-delay&amp;gt;
&amp;lt;redelivery-delay-multiplier&amp;gt;2.0&amp;lt;/redelivery-delay-multiplier&amp;gt;
&amp;lt;max-redelivery-delay&amp;gt;300000&amp;lt;/max-redelivery-delay&amp;gt;
&amp;lt;max-delivery-attempts&amp;gt;-1&amp;lt;/max-delivery-attempts&amp;gt;
&lt;/pre&gt;





&lt;p&gt;These settings provide:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;An initial retry delay.&lt;/li&gt;
	&lt;li&gt;Exponential backoff.&lt;/li&gt;
	&lt;li&gt;A maximum retry interval.&lt;/li&gt;
	&lt;li&gt;Unlimited delivery attempts.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;These values were selected to support the reliable delivery goal demonstrated in the proof of concept.&lt;/p&gt;





&lt;p&gt;A production implementation would tune these values based on operational requirements, expected outage duration, message urgency, and business expectations.&lt;/p&gt;





&lt;p&gt;The important part is that Artemis avoids continuously hammering an unavailable SMTP server while still preserving messages for later delivery.&lt;/p&gt;





&lt;h2&gt;Production Considerations&lt;/h2&gt;





&lt;p&gt;The proof of concept intentionally focuses on a single failure mode:&lt;/p&gt;





&lt;p&gt;&lt;strong&gt;SMTP server unavailable.&lt;/strong&gt;&lt;/p&gt;





&lt;p&gt;That was the original customer scenario, and it is the scenario this proof of concept validates.&lt;/p&gt;





&lt;p&gt;A production implementation would need additional handling for permanent failures.&lt;/p&gt;





&lt;p&gt;Examples include:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;Invalid recipient addresses.&lt;/li&gt;
	&lt;li&gt;Invalid sender addresses.&lt;/li&gt;
	&lt;li&gt;Oversized messages.&lt;/li&gt;
	&lt;li&gt;Malformed MIME content.&lt;/li&gt;
	&lt;li&gt;Domain resolution failures.&lt;/li&gt;
	&lt;li&gt;Mail server policy rejections.&lt;/li&gt;
	&lt;li&gt;Missing required message fields.&lt;/li&gt;
&lt;/ul&gt;





&lt;p&gt;Retrying these messages forever usually doesn't make sense.&lt;/p&gt;





&lt;p&gt;A production implementation would likely inspect delivery failures and make routing decisions based on the type of failure.&lt;/p&gt;





&lt;p&gt;Some messages should remain in the send queue because the failure is transient.&lt;/p&gt;





&lt;p&gt;Some messages should move to a dead letter queue.&lt;/p&gt;





&lt;p&gt;Some messages should move to an undeliverable queue.&lt;/p&gt;





&lt;p&gt;Some messages may require manual review.&lt;/p&gt;





&lt;p&gt;Some messages may be safe to discard according to business policy.&lt;/p&gt;





&lt;p&gt;A more complete production topology might look like this:&lt;/p&gt;





&lt;p&gt;&lt;img data-fileentryid="126071063" src="https://liferay.dev/documents/portlet_file_entry/14/prod-topology.png/554f9320-7d03-4947-8ad7-e0de9ac7c9f2"&gt;&lt;/p&gt;





&lt;p&gt;The correct handling depends entirely on business requirements.&lt;/p&gt;





&lt;p&gt;A password reset email might be discarded after a reasonable expiration window.&lt;/p&gt;





&lt;p&gt;A purchase confirmation email might require manual review.&lt;/p&gt;





&lt;p&gt;A regulatory notification might require alerting and escalation.&lt;/p&gt;





&lt;p&gt;The important point is that these decisions happen after the message has already been durably persisted.&lt;/p&gt;





&lt;p&gt;The email request is never silently lost because of a temporary infrastructure failure.&lt;/p&gt;





&lt;h2&gt;What About Custom Email Code?&lt;/h2&gt;





&lt;p&gt;This solution works automatically for applications using Liferay's standard mail APIs.&lt;/p&gt;





&lt;p&gt;If custom code bypasses Liferay's mail infrastructure and connects directly to SMTP, those messages will not pass through the durable queue.&lt;/p&gt;





&lt;p&gt;Additional customization would be required to bring those emails into the same processing pipeline, but it's not that complicated. You're already preparing a &lt;code&gt;MailMessage&lt;/code&gt; and then invoking &lt;code&gt;Transport.send()&lt;/code&gt; with it. Instead of sending it, take a look at how &lt;code&gt;JmsMailDivertListener&lt;/code&gt; is posting the mail to the JMS queue. Your custom code would do the same thing and the rest of the implementation will just work.&lt;/p&gt;





&lt;p&gt;This is an important operational detail.&lt;/p&gt;





&lt;p&gt;If reliable email delivery matters, you need to understand every path your system uses to send email.&lt;/p&gt;





&lt;h2&gt;Final Thoughts&lt;/h2&gt;





&lt;p&gt;The interesting thing about this project is that it started with an email question and ended with a messaging solution.&lt;/p&gt;





&lt;p&gt;The customer wasn't really asking how to send email more reliably.&lt;/p&gt;





&lt;p&gt;They were asking how to ensure an important business event could not be lost.&lt;/p&gt;





&lt;p&gt;Durable messaging solves that problem.&lt;/p&gt;





&lt;p&gt;And it solves the problem regardless of the scenario, whether it is simply sending an email or updating a record in an external system or writing a file to the filesystem... They're all the same - it's an important business event that cannot be lost, and an outcome that must be reliably satisfied. JMS and durable queues are the way to ensure that.&lt;/p&gt;





&lt;p&gt;For reliable email delivery, once email requests are persisted in a durable queue, temporary infrastructure outages stop being catastrophic events and become operational inconveniences.&lt;/p&gt;





&lt;p&gt;Liferay provides a clean interception point through the Liferay Message Bus.&lt;/p&gt;





&lt;p&gt;JMS provides queue durability.&lt;/p&gt;





&lt;p&gt;Apache Camel provides integration.&lt;/p&gt;





&lt;p&gt;Together they create a relatively simple solution for a problem many organizations assume requires expensive proprietary infrastructure.&lt;/p&gt;





&lt;p&gt;And perhaps the biggest lesson is this:&lt;/p&gt;





&lt;p&gt;When something absolutely must happen, don't rely on immediate execution.&lt;/p&gt;





&lt;p&gt;Publish (enqueue) the intent first.&lt;/p&gt;





&lt;p&gt;Then process it reliably.&lt;/p&gt;





&lt;p&gt;URL for the repo if you didn't see it already:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;&lt;a
      href="https://github.com/dnebing/liferay-reliable-email-delivery" rel="noopener noreferrer"&gt;https://github.com/dnebing/liferay-reliable-email-delivery&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</summary>
    <dc:creator>David H Nebinger</dc:creator>
    <dc:date>2026-06-17T17:51:00Z</dc:date>
  </entry>
  <entry>
    <title>Call for Papers for DevCon - two weeks to go!</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126022454" />
    <author>
      <name>Ben Turner</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126022454</id>
    <updated>2026-06-15T05:27:38Z</updated>
    <published>2026-06-15T05:10:00Z</published>
    <summary type="html">&lt;p&gt;If you've been paying attention recently on liferay.dev you have probably noticed the new banner promoting DevCon. It may still be a few months until our biggest event of the year takes place in London on November 3rd and 4th (with the un-missable Unconference happening on the 5th) - &lt;em&gt;but&lt;/em&gt; - that doesn't mean you can sit back and relax!&lt;/p&gt;




&lt;p&gt;Our &lt;em&gt;Call for Papers&lt;/em&gt; for DevCon 2026 is wrapping up &lt;u&gt;in just two weeks&lt;/u&gt; - on &lt;strong&gt;June 30th&lt;/strong&gt; - and we really want to hear your ideas for possible talks!&lt;/p&gt;




&lt;p&gt;What makes DevCon special is that it's not only an event for anyone that develops with Liferay, or that is interested in Liferay at all, but it's also an event that welcomes content from independent developers, from Liferay Partners, and one where anyone with a strong story about Liferay can directly contribute to.&lt;/p&gt;




&lt;p&gt;This year we're interested in talks that touch on:&lt;/p&gt;




&lt;ul&gt;
	&lt;li&gt;Cloud Technologies&lt;/li&gt;
	&lt;li&gt;Security&lt;/li&gt;
	&lt;li&gt;Building with Liferay&lt;/li&gt;
	&lt;li&gt;Architecture&lt;/li&gt;
	&lt;li&gt;Developer Experience &amp;amp; AI-Augmented Development &lt;/li&gt;
	&lt;li&gt;Integrations&lt;/li&gt;
	&lt;li&gt;Low Code, AI Code &amp;amp; No Code&lt;/li&gt;
	&lt;li&gt;Agentic AI &amp;amp; Automation&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;(There's more detail on these topics in the Call for Papers submission form!)&lt;/p&gt;




&lt;p&gt;And, of all of these, the topic that we're &lt;em&gt;really&lt;/em&gt; interested in is how you're using AI to implement Liferay and expand it's capabilities. Maybe you're using Gemini to help you better understand how Liferay works, or you're using Claude to build Client Extensions, or using OpenAI to design your look and feel. Whatever tooling, models, or harnesses you're working with - if it's doing something with Liferay we would love to hear you talk about it!&lt;/p&gt;




&lt;p&gt;Check out the Event site &lt;a
    href="https://events.liferay.com/event/devcon-26/Home?utm_campaign=dev.liferay&amp;amp;utm_medium=organic&amp;amp;RefId=dev.liferay&amp;amp;utm_source=dev.liferay"
    rel="noopener noreferrer" target="_blank"&gt;here&lt;/a&gt;, where you'll find a link to the Call for Papers Form. Don't miss out on your opportunity to present your ideas to Liferay Developers from all over the world!&lt;/p&gt;




&lt;p&gt;Cheers!&lt;/p&gt;




&lt;p&gt;Ben&lt;/p&gt;</summary>
    <dc:creator>Ben Turner</dc:creator>
    <dc:date>2026-06-15T05:10:00Z</dc:date>
  </entry>
  <entry>
    <title>Alternative tool for migrating Liferay to PostgreSQL</title>
    <link rel="alternate" href="/c/blogs/rss&amp;entryId=126015368" />
    <author>
      <name>Carlos Durán</name>
    </author>
    <id>/c/blogs/rss&amp;entryId=126015368</id>
    <updated>2026-06-14T23:51:27Z</updated>
    <published>2026-06-13T22:37:00Z</published>
    <summary type="html">&lt;p data-path-to-node="1"&gt;For some time now, PostgreSQL has been gaining so much ground that even Liferay itself has made the decision to replace MySQL with PostgreSQL.&lt;/p&gt;





&lt;p data-path-to-node="2"&gt;At this point, many of us are faced with the non-trivial task of performing the same operation in our on-premise installations and, above all, how to do it properly.&lt;/p&gt;





&lt;p data-path-to-node="3"&gt;Liferay published a &lt;a data-hveid="0"
    data-ved="0CAAQ_4QMahcKEwjGstSUrIWVAxUAAAAAHQAAAAAQUw"
    href="https://learn.liferay.com/kb-article/migrating-your-database-to-postgresql"
    rel="noopener noreferrer" target="_blank"&gt;tool&lt;/a&gt; for this purpose, divided into two parts:&lt;/p&gt;





&lt;ul&gt;
	&lt;li&gt;
	&lt;p
      data-path-to-node="4,0,0"&gt;The data exporter&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p data-path-to-node="4,1,0"&gt;The migration script&lt;/p&gt;
	&lt;/li&gt;
&lt;/ul&gt;





&lt;p data-path-to-node="5"&gt;Both my colleagues and I have tried to use this tool to perform this operation, but the process (at least in my case) did not achieve the desired outcome. In fact, Liferay DXP 2026 already warns in the control panel that this tool is in its Beta phase.&lt;/p&gt;





&lt;p data-path-to-node="6"&gt;Faced with this inconvenience, I have been running tests to develop a tool that allows this operation to be performed.&lt;/p&gt;





&lt;h2&gt;Requirements&lt;/h2&gt;





&lt;p
    data-path-to-node="8"&gt;To carry out the procedure, you must have downloaded the &lt;a
    data-hveid="0"
    data-ved="0CAAQ_4QMahcKEwjGstSUrIWVAxUAAAAAHQAAAAAQVA"
    href="https://github.com/formatocd/liferay-db-migrator/releases"
    rel="noopener noreferrer" target="_blank"&gt;tool&lt;/a&gt;, have an empty PostgreSQL database, and have Java 21 installed.&lt;/p&gt;





&lt;h2&gt;Procedure&lt;/h2&gt;





&lt;p data-path-to-node="10"&gt;The procedure to perform the migration with this tool is as follows:&lt;/p&gt;





&lt;ol&gt;
	&lt;li&gt;
	&lt;p
      data-path-to-node="11,0,0"&gt;Create an empty database on a PostgreSQL server&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p
      data-path-to-node="11,1,0"&gt;Stop the application server of the Liferay installation&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p
      data-path-to-node="11,2,0"&gt;Change the database connection configuration to PostgreSQL&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p
      data-path-to-node="11,3,0"&gt;Start the server again&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p
      data-path-to-node="11,4,0"&gt;Wait for the table creation process to finish&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p
      data-path-to-node="11,5,0"&gt;Stop the server again&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p
        data-path-to-node="11,6,0"&gt;Run the tool (see &lt;a data-hveid="0"
        data-ved="0CAAQ_4QMahcKEwjGstSUrIWVAxUAAAAAHQAAAAAQVQ"
        href="https://github.com/formatocd/liferay-db-migrator/blob/main/README.md"
        rel="noopener noreferrer"
      target="_blank"&gt;documentation&lt;/a&gt;)&lt;/p&gt;
	&lt;/li&gt;
	&lt;li&gt;
	&lt;p data-path-to-node="11,7,0"&gt;Start the server again&lt;/p&gt;
	&lt;/li&gt;
&lt;/ol&gt;





&lt;p data-path-to-node="12"&gt;Once the entire procedure is completed, the Liferay installation will start up using the PostgreSQL database with all the data transferred.&lt;/p&gt;</summary>
    <dc:creator>Carlos Durán</dc:creator>
    <dc:date>2026-06-13T22:37:00Z</dc:date>
  </entry>
</feed>
