Local Execution & Operations Guide (Outside of LDM)¶
This runbook guides you through executing the Liferay AI Commerce Accelerator (AICA) locally against an existing or restored Liferay DXP environment without using Liferay Docker Manager (LDM) or downloading .ldmp packages.
1. Architecture Overview¶
When running outside of LDM, the system operates with three distinct components:
┌─────────────────────────┐ REST & Batch APIs ┌─────────────────────────┐
│ Liferay DXP │ ◄────────────────────────────────── │ AICA Microservice │
│ (Tomcat Bundle/Docker) │ │ (Node.js Daemon :3001) │
│ Port :8080 │ ──────────────────────────────────► │ │
└─────────────────────────┘ WebSockets └────────────┬────────────┘
▲ │
│ Client Extensions (Batch / UI) │
└───────────────────────────────────────────────────────────────┘
▲
│ Control & Seeding
┌────────────┴────────────┐
│ CLI / Web Control Panel │
│ (Port 3001 / CLI) │
└─────────────────────────┘
- Target DXP Instance (Port
8080): The running Liferay portal hosting commerce catalogs, channels, and Object definitions. - AICA Microservice (Port
3001): The Node.js orchestrator that connects to DXP via REST APIs, handles AI generation, and coordinates asynchronous batch engine jobs. - Client Extensions: The data definitions (
batch), management UI (configuration), and storefront widgets (frontendandsite-initializer) deployed to DXP. - AICA CLI & Control Center: Zero-dependency scripts to trigger seeding, inspect connection status, or manage sessions.
2. Prerequisites¶
- Java JDK: Version 11 or 21 (LTS).
- Node.js:
v22.22.2(pinned viabuild.gradle). - Yarn:
v1.22+. - Liferay Blade CLI or the bundled
./gradlewwrapper.
3. Step 1: Prepare the Target Liferay DXP Instance¶
Required Feature Flag¶
AICA requires the Page Management API feature flag to automatically scaffold demo pages and bind templates:
Ensure portal-ext.properties contains:
For a smooth local experience (bypassing password reset prompts and setup wizards), you can use the repository defaults located in configs/common/portal-ext.properties:
setup.wizard.enabled=false
passwords.default.policy.change.required=false
terms.of.use.required=false
company.security.strangers.verify=false
users.reminder.queries.enabled=false
default.admin.password=test
feature.flag.LPD-35443=true
Starting Liferay DXP Locally¶
- Using the local Tomcat bundle (
bundles/):
(Or run blade server run from the project root).
- Using standalone Docker (without LDM):
docker run -it -m 8g -p 8080:8080 \
-e LIFERAY_SETUP_PERIOD_WIZARD_PERIOD_ENABLED=false \
-e LIFERAY_PASSWORDS_PERIOD_DEFAULT_PERIOD_POLICY_PERIOD_CHANGE_PERIOD_REQUIRED=false \
-e LIFERAY_DEFAULT_PERIOD_ADMIN_PERIOD_PASSWORD=test \
-e LIFERAY_FEATURE_PERIOD_FLAG_PERIOD__UPPERCASEL__UPPERCASEP__UPPERCASED__MINUS__35443=true \
-v "$(pwd)/bundles/osgi/client-extensions:/opt/liferay/osgi/client-extensions" \
liferay/dxp:2026.q1.12-lts
Shared OSGi modules¶
Some capabilities are provided by OSGi modules from
liferay-custom-osgi-modules,
because no Headless API exposes them. They are consumed as published release
artifacts rather than built here:
./gradlew deploy runs this for you. gradle.properties pins a release
(aica.shared.osgi.release) rather than a bundle version. Each release
publishes a modules.json naming every bundle's version, asset and checksum,
so one request resolves them all and this build never has to assemble a
filename from naming rules owned by another repository. Requires v3.0.0 or
later; earlier releases have no manifest.
Two jars sharing a Bundle-SymbolicName is a duplicate-bundle conflict, so the
task removes any other version of a consumed bundle. It also removes bundles a
consumed one replaces — the manifest records renames, and a renamed bundle
is a different bundle to Liferay, so its old jar would otherwise sit alongside
the new one with both registering the same endpoints. Only predecessors of
bundles listed in sharedOsgiBundles are pruned; another adopter's rename is
not this workspace's business.
The published artifacts and this workspace must be on the same DXP line.
aica.shared.osgi.dxp.line is asserted against the release manifest, and the
build additionally refuses to run when it diverges from
liferay.workspace.product, so the two cannot drift apart unnoticed.
That enforcement replaced an earlier rule that let them differ, on the grounds
that these modules import only com.liferay.portal.kernel.*, whose package
majors were held to be stable across lines. That reasoning is wrong. It
survived q1.7-lts to q1.12-lts by luck and then failed outright: between
dxp-2026.q1.12-lts and 2026.Q3.0 the kernel exports moved
| package | declared range | Q3.0 exports |
|---|---|---|
com.liferay.portal.kernel.model |
[48.0,49.0) |
51.0.0 |
com.liferay.portal.kernel.search |
[22.0,23.0) |
24.2.0 |
com.liferay.portal.kernel.service |
[52.0,53.0) |
54.1.0 |
com.liferay.portal.kernel.util |
[96.0,97.0) |
100.0.0 |
and every consumed bundle failed to resolve on a Q3.0 portal. Two lessons are worth carrying:
- The ranges are hardcoded in each module's
bnd.bndupstream. Retargeting a build does not recompute them; they have to be edited, so "rebuild against the new line" is not on its own a fix. - The OSGi resolver reports only the first unsatisfied requirement and
stops. One package named in a
Could not resolve moduleerror is not evidence that only one range is stale — readExport-Packagefrom the target line's kernel JAR instead of inferring scope from the log. - An import with no version range at all is worse than a stale one. It
satisfies any exported version, so it resolves, the bundle starts, and the
endpoint registers — the probe below returns
403, exactly as a healthy module does. Nothing is wrong until the first call that touches a changed signature, which then fails as a linkage error (NoSuchMethodError,NoClassDefFoundError) rather than a resolution error. The symptom is deferred, not absent, which is why neither the boot log nor the probe can substitute for the upstream check. That check is now per package; it previously grepped the wholeImport-Packageblob, so one versioned import vouched for every unversioned one beside it.
A bundle that fails to resolve is quiet at request time — the endpoint simply returns 404 — but it is not silent at install time. Every boot logs
ERROR [DirectoryWatcher:989] Unable to start bundle: .../<bundle>.jar
org.osgi.framework.BundleException: Could not resolve module: <bsn>
Unresolved requirement: Import-Package: ...
so for a range mismatch the boot log is the cheapest place to check, ahead of probing an endpoint. A clean boot log rules out a mismatch, not an unversioned import — that failure is invisible at both places. To confirm one is live:
404 means the bundle is not resolved. 403 means it is registered and
rejected the unauthenticated request, which is the expected response.
client-extension-entry has the same probe at
/o/client-extension-entry/status. It backs the Dashboard's Adjust
Configuration button: the button's URL needs the portlet id Liferay composes
for the configuration client extension, and that id embeds the company id,
which Liferay assigns per database and no API outside the portal JVM publishes.
The microservice reads it through this module rather than carrying a literal id
that is only correct on the instance it was copied from. Without the module the
Configuration Doctor says so and the button falls back to the Client Extensions
listing, so nothing breaks — the reader just needs one more click. See #660.
This module also imports com.liferay.client.extension.* application packages,
not only com.liferay.portal.kernel.*, which was long assumed to make it the
line-sensitive one of the set. It isn't: on 2026.Q3.0 every
com.liferay.client.extension.* range it declares still satisfies, and it
failed there for the same kernel-package reason as the other two. No module in
this set is line-agnostic, so the line has to match regardless of which
packages a module imports.
4. Step 2: Build & Deploy Client Extensions to DXP¶
All client extensions (including the ai-commerce-accelerator-batch extension that defines the Liferay Objects and prompts) must be deployed to the running Liferay instance:
(Or blade gw clean deploy).
This command automatically:
- Executes
generateBatchFilesto build minified schemas and prompt payloads intoclient-extensions/ai-commerce-accelerator-batch/batch/. - Compiles frontend and configuration assets.
- Packages each client extension into
.zipbundles and deploys them tobundles/osgi/client-extensions/.
5. Step 3: Configure Environment & Launch the Microservice¶
Configure .env¶
Create or update .env in the project root to target your DXP instance:
# Target DXP Endpoint
LIFERAY_API_URL=http://localhost:8080
LIFERAY_URL=http://localhost:8080
COM_LIFERAY_LXC_DXP_MAIN_DOMAIN=localhost:8080
COM_LIFERAY_LXC_DXP_SERVER_PROTOCOL=http
# Administrative Basic Authentication
LIFERAY_API_USERNAME=test@liferay.com
LIFERAY_API_PASSWORD=test
# AI Provider API Key (OpenAI, Gemini, or Anthropic)
AI_API_KEY=sk-...
Launch the Microservice Daemon¶
From the project root, you can start the service using the interactive launcher:
Select [1] Start & Open Local Dashboard UI to boot the microservice on http://localhost:3001 and automatically open your browser.
Alternatively, start the server directly:
Starting via Gradle, so OAuth credentials are discovered¶
Prefer the Gradle task when the client extensions are deployed to a local bundle:
Use packageRunDebug instead to attach a debugger.
The packageRun* tasks are not merely a wrapper around yarn start. The
Liferay Workspace plugin sets LIFERAY_ROUTES_CLIENT_EXTENSION and
LIFERAY_ROUTES_DXP on them, pointing at the configuration Liferay writes when
a client extension is deployed:
That directory holds the OAuth client id and secret, so the microservice
authenticates without any credentials in .env. Starting with yarn start or
node server.cjs bypasses the injection, leaving nothing for OAuth discovery
to read — which presents as an authentication failure that looks like a wrong
client secret.
If you must start it outside Gradle, either set those two variables yourself
(see .env.example) or set LIFERAY_AUTH_METHOD=basic to use
LIFERAY_API_USERNAME and LIFERAY_API_PASSWORD instead.
6. Step 4: Execute Data Generation¶
Verify Connection¶
Test the handshake and taxonomy inspection:
Option A: Headless Seeding via CLI¶
Generate 30 products, accounts, and historical orders:
node scripts/aica-cli.cjs generate \
--products 30 \
--accounts 4 \
--orders 10 \
--bulk-pricing \
--tier-pricing \
--specifications
(To run an offline mock test without consuming live AI tokens, add the --demo flag).
Option B: Interactive UI Generation¶
- Navigate to
http://localhost:3001in your browser. - In the Data Generator form:
-
Brand / Context: Provide the name and domain description of the catalog to seed, e.g.:
-
Product Count: Target product volume (e.g.
30). - Account Count: Target account volume for B2B/B2C entities (e.g.
4). - Order Count: Target historical order volume (e.g.
10). - If using custom persona and ordering rules, customize the prompt templates in the Configuration Panel:
ai-prompt-product: Define category lines, option matrices (colors, sizes), and key-value technical specifications (e.g. MATERIAL, CAPACITY, COMPATIBILITY).ai-prompt-account: Define customer persona types, regional distribution, and B2B dealer profiles with contact roles.ai-prompt-order: Define purchase behaviors, date distribution ranges, and targeted cross-sell / upsell purchasing patterns across accounts.ai-prompt-image: Define the photographic style, framing, and background treatment applied to generated product imagery.- Click Generate Data. The progress bars track real-time batch creation, option linking, and taxonomy grounding via WebSockets.
7. Targeting Remote Liferay Instances (PaaS / SaaS / Cloud Environments)¶
When populating commerce data for a remote Liferay PaaS (LCP) or Experience Cloud environment, you run the AICA microservice and CLI locally while executing outbound API requests directly against the remote portal URL. Because requests originate server-to-server from the Node.js daemon, browser CORS constraints are completely bypassed.
Remote Instance Prerequisites¶
- Enable Feature Flag: In your remote Liferay instance, go to Control Panel -> System Settings -> Platform -> Feature Flags and enable
LPD-35443(Page Management API). - Deploy Object Definitions: Ensure the
ai-commerce-accelerator-batchclient extension is deployed to the remote instance so the required Liferay Object definitions (c_aicaconfiguration, etc.) are active. - Authentication Profile: Create an OAuth2 Headless Server profile in the remote Liferay Cloud portal:
- Go to Global Menu ➔ Control Panel ➔ Security ➔ OAuth 2 Administration.
- Click the Add (+) button.
- Configure:
- Application Name:
AICA Microservice Remote Seeder - Client Profile:
Headless Server - Allowed Grant Types: Check
Client Credentials
- Application Name:
- Click Save, then copy the generated Client ID and Client Secret.
- Open the Scopes tab and enable the following scopes:
Liferay.Headless.Commerce.Admin.Catalog.everythingLiferay.Headless.Commerce.Admin.Channel.everythingLiferay.Headless.Commerce.Admin.Inventory.everythingLiferay.Headless.Commerce.Admin.Order.everythingLiferay.Headless.Commerce.Admin.Pricing.everythingLiferay.Headless.Delivery.everythingLiferay.Headless.Admin.User.everythingLiferay.Headless.Admin.Address.everythingLiferay.Headless.Batch.Engine.everythingc_aicaconfiguration.everything
- Click Save.
Configure Remote .env¶
Point your local .env to the remote PaaS hostname:
# Remote Liferay PaaS / Cloud URL
LIFERAY_API_URL=https://webserver-myproject-prd.lfr.cloud
LIFERAY_URL=https://webserver-myproject-prd.lfr.cloud
COM_LIFERAY_LXC_DXP_MAIN_DOMAIN=webserver-myproject-prd.lfr.cloud
COM_LIFERAY_LXC_DXP_SERVER_PROTOCOL=https
# Basic Auth Credentials (Optional Fallback)
LIFERAY_API_USERNAME=admin@example.com
LIFERAY_API_PASSWORD=your-remote-password
# OAuth2 Client Credentials Flow (Recommended)
LIFERAY_OAUTH_CLIENT_ID=your-client-id
LIFERAY_OAUTH_CLIENT_SECRET=your-client-secret
# AI Provider Key
AI_API_KEY=sk-...
Naming the Configuration Source Separately¶
Step 2 above - deploying the batch client extension to the remote instance so
c_aicaconfiguration exists there - is a requirement only because AICA used to
read its own configuration from the instance it writes to. That is the right
place for catalog and channel ids, categories and exclude lists, which describe
the instance. It is the wrong place for the AI settings, prompts, chunk sizes
and request timeouts, which describe how the microservice does one job.
If the remote instance has no AICA configuration object - which is the usual case when the client extensions are deployed only to a local bundle - name the instance that does:
# Where AICA reads its own settings from. Blank means "the same instance".
AICA_CONFIG_SOURCE_URL=http://localhost:8080
AICA_CONFIG_SOURCE_CLIENT_ID=your-local-client-id
AICA_CONFIG_SOURCE_CLIENT_SECRET=your-local-client-secret
The same connection can be entered per run in the Configuration source row of the setup rail, which outranks these variables - on a shared deployment the environment is one value for every user of the server.
Two rules follow, and both are deliberate:
- A configuration source on a different host needs its own Client ID and Client Secret. The target's credentials are not valid there, so they are refused rather than reused.
- A configuration source that cannot be read stops the run and says which instance it could not read. No default is substituted. A setting that silently falls back to a built-in value cannot be corrected, which is the defect this exists to remove - see #824.
Execute Against Remote Instance¶
Option A: Interactive Local UI (Targeting Remote Cloud)¶
- Launch the local microservice via
./start.sh(Option [1]) oryarn start. - Open
http://localhost:3001in your browser. - In the Connection & Authentication card, enter:
- Liferay URL:
https://webserver-myproject-prd.lfr.cloud - Microservice URL:
http://localhost:3001 - Client ID: Your remote OAuth2 Client ID
- Client Secret: Your remote OAuth2 Client Secret
- Click Test Connection / Connect. Once the status indicator turns green, use the Data Generator form to trigger seeding. The local microservice executes all requests server-to-server directly into your remote PaaS instance.
Option B: Headless CLI¶
- Test Remote Handshake & Channel Inspection:
AICA will query the remote instance's live channels, sites, and currency settings.
- Trigger Seeding:
(For full bi-directional communication with custom element microfrontends or webhook callbacks on remote SaaS instances, see SaaS_TARGETING_GUIDE.md).
8. Step 5: Teardown & Reset Operations¶
To delete all generated products, accounts, catalogs, and reset the local microservice session state:
(Or select [4] Clean / Teardown All Generated Data in ./start.sh).
9. Operational Troubleshooting¶
401 Account Lockout Loop¶
Liferay locks out accounts after 5 failed login attempts during rapid polling. If test@liferay.com becomes locked, unlock the user in your database:
UPDATE user_ SET lockout = false, lockoutDate = null, failedLoginAttempts = 0 WHERE emailaddress = 'test@liferay.com';
Search Indexing Latency¶
If newly created products or Object configurations do not immediately appear in searches or dropdowns, trigger a search reindex:
(Or in DXP: Control Panel -> Search -> Index Actions -> Reindex All).
Last Reviewed: 2026-09-18