Skip to content

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 (frontend and site-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 via build.gradle).
  • Yarn: v1.22+.
  • Liferay Blade CLI or the bundled ./gradlew wrapper.

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:

feature.flag.LPD-35443=true

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/):
./bundles/tomcat/bin/catalina.sh run

(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 downloadSharedOsgiModules

./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.bnd upstream. 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 module error is not evidence that only one range is stale — read Export-Package from 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 whole Import-Package blob, 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:

curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/o/commerce-site-type/status

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:

./gradlew clean deploy

(Or blade gw clean deploy).

This command automatically:

  1. Executes generateBatchFiles to build minified schemas and prompt payloads into client-extensions/ai-commerce-accelerator-batch/batch/.
  2. Compiles frontend and configuration assets.
  3. Packages each client extension into .zip bundles and deploys them to bundles/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:

./start.sh

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:

cd client-extensions/ai-commerce-accelerator-microservice
yarn start

Starting via Gradle, so OAuth credentials are discovered

Prefer the Gradle task when the client extensions are deployed to a local bundle:

./gradlew :client-extensions:ai-commerce-accelerator-microservice:packageRunStart

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:

bundles/routes/default/ai-commerce-accelerator-microservice
bundles/routes/default/dxp

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:

node scripts/aica-cli.cjs connect

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

  1. Navigate to http://localhost:3001 in your browser.
  2. In the Data Generator form:
  3. Brand / Context: Provide the name and domain description of the catalog to seed, e.g.:

    Acme Industrial Supplies — Commercial power tools, heavy-duty safety gear, and industrial shop equipment.
    
  4. Product Count: Target product volume (e.g. 30).

  5. Account Count: Target account volume for B2B/B2C entities (e.g. 4).
  6. Order Count: Target historical order volume (e.g. 10).
  7. If using custom persona and ordering rules, customize the prompt templates in the Configuration Panel:
  8. ai-prompt-product: Define category lines, option matrices (colors, sizes), and key-value technical specifications (e.g. MATERIAL, CAPACITY, COMPATIBILITY).
  9. ai-prompt-account: Define customer persona types, regional distribution, and B2B dealer profiles with contact roles.
  10. ai-prompt-order: Define purchase behaviors, date distribution ranges, and targeted cross-sell / upsell purchasing patterns across accounts.
  11. ai-prompt-image: Define the photographic style, framing, and background treatment applied to generated product imagery.
  12. 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

  1. Enable Feature Flag: In your remote Liferay instance, go to Control Panel -> System Settings -> Platform -> Feature Flags and enable LPD-35443 (Page Management API).
  2. Deploy Object Definitions: Ensure the ai-commerce-accelerator-batch client extension is deployed to the remote instance so the required Liferay Object definitions (c_aicaconfiguration, etc.) are active.
  3. Authentication Profile: Create an OAuth2 Headless Server profile in the remote Liferay Cloud portal:
  4. Go to Global Menu ➔ Control Panel ➔ Security ➔ OAuth 2 Administration.
  5. Click the Add (+) button.
  6. Configure:
    • Application Name: AICA Microservice Remote Seeder
    • Client Profile: Headless Server
    • Allowed Grant Types: Check Client Credentials
  7. Click Save, then copy the generated Client ID and Client Secret.
  8. Open the Scopes tab and enable the following scopes:
    • Liferay.Headless.Commerce.Admin.Catalog.everything
    • Liferay.Headless.Commerce.Admin.Channel.everything
    • Liferay.Headless.Commerce.Admin.Inventory.everything
    • Liferay.Headless.Commerce.Admin.Order.everything
    • Liferay.Headless.Commerce.Admin.Pricing.everything
    • Liferay.Headless.Delivery.everything
    • Liferay.Headless.Admin.User.everything
    • Liferay.Headless.Admin.Address.everything
    • Liferay.Headless.Batch.Engine.everything
    • c_aicaconfiguration.everything
  9. 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)

  1. Launch the local microservice via ./start.sh (Option [1]) or yarn start.
  2. Open http://localhost:3001 in your browser.
  3. In the Connection & Authentication card, enter:
  4. Liferay URL: https://webserver-myproject-prd.lfr.cloud
  5. Microservice URL: http://localhost:3001
  6. Client ID: Your remote OAuth2 Client ID
  7. Client Secret: Your remote OAuth2 Client Secret
  8. 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

  1. Test Remote Handshake & Channel Inspection:
node scripts/aica-cli.cjs connect

AICA will query the remote instance's live channels, sites, and currency settings.

  1. Trigger Seeding:
node scripts/aica-cli.cjs generate --products 30 --accounts 4 --orders 10

(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:

node scripts/aica-cli.cjs delete --all

(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:

node scripts/aica-cli.cjs reindex

(Or in DXP: Control Panel -> Search -> Index Actions -> Reindex All).


Last Reviewed: 2026-09-18