Loading...
Loading...
Integrate App Builder Database Storage (@adobe/aio-lib-db) into an Adobe Commerce app and scaffold a runtime action that reads and writes documents. Use when the user wants persistent, queryable storage backing a Commerce app — either from a web action (HTTP-invokable) or from an event/webhook handler. Requires a base app initialized with commerce-app-init.
npx skill4agent add adobe/skills commerce-app-storage@adobe/aio-lib-dbweb: "yes"responses@adobe/aio-commerce-lib-coreapp.commerce.config.tscommerce-app-eventingruntimeActionscommerce-app-webhooksruntimeActionapp.commerce.config.tssrc/commerce-extensibility-1/node_modules@adobe/aio-commerce-lib-appapp.commerce.config.tscommerce-app-initsrc/commerce-extensibility-1/node_modulesnpx @adobe/aio-commerce-lib-app initwebpack-config.cjstsconfig.jsoninitcommerce-app-initAppBuilderDataServicesSDKapp.config.yamlaio app deployapplication:
runtimeManifest:
database:
auto-provision: true
region: emea # amer | apac | emea | aus — the single source of truth for the regionaio appaio-lib-dbaio app deployapplicationextensionsapplicationapplicationpost-app-buildapplication:
hooks:
post-app-build: "mkdir -p dist/application/actions" # provisioning expects this dir to exist
runtimeManifest:
packages: {} # empty map — required by the config schema so the application block validates with no actions
database:
auto-provision: true
region: emea # single source of truth — see the region callout belowaio app runaio app devaio app db …aio plugins install @adobe/aio-cli-plugin-app-storage
aio app db provision --region <amer|apac|emea|aus>Region is a single source of truth. Thein the manifestregionblock must match thedatabasepassed to everyregioncall (orinitDb({ region })) — in every action and in the install step (Step 6). A mismatch fails the connection. Changing region is destructive:AIO_DB_REGION, updateaio app db deletein the manifest, then re-provision.database.region
npm install @adobe/aio-lib-dbdatabase.regioninit()AIO_DB_REGIONsrc/commerce-extensibility-1/ext.config.yamlapp-managementis required on every DB action. Without it,include-ims-credentials: truehas no IMS token to authenticate with and the connection fails at runtime (and the app installation fails if the action runs during install). Do not omit this annotation.aio-lib-db
# src/commerce-extensibility-1/ext.config.yaml
runtimeManifest:
packages:
app-management:
# ... auto-generated — do not edit
my-app: # any name except "app-management"
actions:
store-record:
function: actions/store-record/index.js # relative to src/commerce-extensibility-1/
runtime: nodejs:24
web: "yes" # "yes" for a web action; "no" for an event/webhook action
annotations:
include-ims-credentials: true # REQUIRED for aio-lib-db auth| Field | Constraint |
|---|---|
| Package name | Lowercase alphanumeric + hyphens; never |
| Path relative to |
| Must be |
| |
| Collection name | Non-empty string; created on first write if it doesn't exist |
| Region | Must match the manifest |
initconnectclosefinally// Web action — src/commerce-extensibility-1/actions/store-record/index.ts
import { buildErrorResponse, ok } from "@adobe/aio-commerce-lib-core/responses";
import {
getImsAuthProvider,
resolveImsAuthParams,
} from "@adobe/aio-commerce-lib-auth";
import { init as initDb } from "@adobe/aio-lib-db";
export async function main(params: Record<string, unknown>) {
let client;
try {
// Resolve the injected AIO_COMMERCE_AUTH_IMS_* params, then mint a raw token string.
const authProvider = getImsAuthProvider(resolveImsAuthParams(params));
const token = await authProvider.getAccessToken();
const db = await initDb({ token, region: "emea" }); // must match the manifest database.region
client = await db.connect();
const records = client.collection("records");
const result = await records.insertOne({
...(params.document as object),
createdAt: new Date().toISOString(),
});
return ok({ body: { result } });
} catch (error: any) {
return buildErrorResponse(error.statusCode || 500, {
body: { message: error.message },
});
} finally {
if (client) await client.close(); // always close — avoids connection leaks
}
}web: "no"params.data// Event/webhook handler — same init/connect/close lifecycle
export async function main(params: Record<string, unknown>) {
const data = params.data as Record<string, unknown>;
let client;
try {
const authProvider = getImsAuthProvider(resolveImsAuthParams(params));
const token = await authProvider.getAccessToken();
const db = await initDb({ token, region: "emea" });
client = await db.connect();
await client
.collection("orders")
.insertOne({ orderId: data.order_id, receivedAt: new Date() });
return ok({ body: { processed: true } });
} finally {
if (client) await client.close();
}
}defineCustomInstallationStepinstalluninstallcontext.paramsconfigclosecreateIndex// ./scripts/setup-database.ts — referenced from config as ./scripts/setup-database.ts
import { defineCustomInstallationStep } from "@adobe/aio-commerce-lib-app/management";
import {
getImsAuthProvider,
resolveImsAuthParams,
} from "@adobe/aio-commerce-lib-auth";
import { init as initDb } from "@adobe/aio-lib-db";
export default defineCustomInstallationStep({
install: async (config, context) => {
let client;
try {
// context.params carries the injected IMS credentials — NOT config.
const authProvider = getImsAuthProvider(
resolveImsAuthParams(context.params),
);
const token = await authProvider.getAccessToken();
const db = await initDb({ token, region: "emea" }); // must match the manifest database.region
client = await db.connect();
const orders = client.collection("held_orders"); // get the collection object first
await orders.createIndex({ order_id: 1 }, { unique: true }); // createIndex on the collection, not a name string
return { status: "success" };
} finally {
if (client) await client.close(); // always close — avoids connection leaks
}
},
uninstall: async (config, context) => {
// Tear down your database state here.
// Leave empty to preserve data across reinstalls.
},
});Author the install script as an ES module with— neverexport default. The installation action loads each step viamodule.exportsand readsimport * as step from "<script>", so the script must default-export thestep.defaultresult. CommonJS breaks this:defineCustomInstallationStep(...)surfaces asmodule.exports.defaultand validation fails. Thestep.default.defaultpath must end inscriptor.js— author it directly in TypeScript, no separate compile step needed..ts
app.commerce.config.tsinstallation.customInstallationStepsscript.ts// app.commerce.config.ts
installation: {
customInstallationSteps: [
{
script: "./scripts/setup-database.ts",
name: "Set up held-orders collection",
description: "Creates the held_orders collection and a unique index on order_id",
},
],
},| Field | Constraint |
|---|---|
| Path relative to the project root; must be an ES module ( |
| Non-empty string, ≤ 255 characters; unique across all installation steps |
| Non-empty string, ≤ 255 characters |
aio app buildaio app deployfinallydatabase.regioninit().project({ field: 1 })createIndexfor await (const doc of collection.find(...))toArray()AIO_DB_REGION@adobe/aio-sdk@adobe/aio-commerce-lib-auth@adobe/aio-lib-core-loggingdefineCustomInstallationSteppost-app-deployinclude-ims-credentials: trueaio app deployaio appaio-lib-dbapplicationpackages: {}post-app-build: "mkdir -p dist/application/actions"applicationdatabase.regionaio app db deletedatabase.regionaio app deploy_idnew ObjectId(idString)bsonObjectIdDbErrorname === "DbError"context.paramsresolveImsAuthParams(context.params)AIO_COMMERCE_AUTH_IMS_*config@adobe/aio-commerce-lib-auth@adobe/aio-lib-core-authgenerateAccessTokenclientIdclientSecretmust export a default function or objectexport defaultmodule.exportsmodule.exports.defaultimport * as.default.defaultcreateIndexclient.collection("name").createIndex({ field: 1 })createIndexaio app buildinclude-ims-credentials: truefinallydatabasecommerce-app-eventingruntimeActionscommerce-app-webhooksruntimeActioncommerce-app-admin-uicontext.paramscreateIndex