
commerce-app-storage
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.
- Installs
- 64
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/adobe/aio-commerce-sdk --skill commerce-app-storage- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads commerce-app-storage automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
Add Database Storage to a Commerce App
Integrates App Builder Database Storage into an existing Commerce app and scaffolds a runtime action that uses @adobe/aio-lib-db to read and write documents. The library is MongoDB-like: data lives in collections of documents, queried with familiar filters.
The db-access code is identical regardless of action type — what differs is how the action is registered and what its handler returns:
- Web action — HTTP-invokable (
web: "yes"); returns a response built with theresponseshelpers from@adobe/aio-commerce-lib-core. - Event/webhook action — invoked by a Commerce event or webhook; referenced from
app.commerce.config.tsviacommerce-app-eventing(runtimeActions) orcommerce-app-webhooks(runtimeAction).
Prerequisites
- Verify the app is scaffolded and initialized, not merely that the config exists. Require both:
app.commerce.config.tspresent in the project root, and- the project initialized — signalled by the generated
src/commerce-extensibility-1/directory and installednode_modules(the@adobe/aio-commerce-lib-appdependency).
- If
app.commerce.config.tsis missing, stop and invokecommerce-app-initfirst (it writes the config, then runs init). - If the config is present but the project is not initialized (no
src/commerce-extensibility-1/ornode_modules), runnpx @adobe/aio-commerce-lib-app initbefore continuing. Init is idempotent — it finds the existing config, skips the interactive prompts, installs dependencies, and generates the project files. - Actions and custom installation scripts can be authored in TypeScript only once the project has the TypeScript build setup (
webpack-config.cjs+ roottsconfig.json) thatinitscaffolds for a TypeScript Commerce config — seecommerce-app-init. Otherwise, author them in JavaScript. - The App Builder Data Services API (API code
AppBuilderDataServicesSDK) must be added to the project in the Adobe Developer Console — in every workspace that uses the database (no special license beyond App Builder). Without it, runtime actions cannot authenticate to the database service.
Step 1 — Provision the workspace database
There is a strict one-to-one relationship between an AIO project workspace and a workspace database. The recommended way to provision it is declaratively in app.config.yaml — the database is provisioned (if not already present) on aio app deploy:
application:
runtimeManifest:
database:
auto-provision: true
region: emea # amer | apac | emea | aus — the single source of truth for the region
Extension-only apps need a workaround. Due to a bug in the aio app CLI plugin (not aio-lib-db), aio app deploy only runs declarative auto-provision when the application runtime manifest has at least one package with a runtime action. Apps built purely with extensions (the recommended layout per the submission guidelines) have no application actions, so deploy silently skips provisioning. Make the application block "real enough" for provisioning to run by adding an empty packages map and a post-app-build hook that creates the directory the provisioning step expects:
application:
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 below
For local development, declarative auto-provisioning does not run during aio app run / aio app dev. Provision once up front with the CLI fallback (self-service, no special permissions). The aio app db … commands are only available once the storage CLI plugin is installed:
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. The
regionin the manifestdatabaseblock must match theregionpassed to everyinitDb({ region })call (orAIO_DB_REGION) — in every action and in the install step (Step 6). A mismatch fails the connection. Changing region is destructive:aio app db delete, updatedatabase.regionin the manifest, then re-provision.
Step 2 — Install the library
npm install @adobe/aio-lib-db
Step 3 — Understand intent
Gather from the user:
- Action type: web action or event/webhook action (see the two shapes above).
- Collection name and the operations needed (insert / find / update / delete).
- Region: must match the manifest
database.region(see the region callout in Step 1). Pass it toinit()or setAIO_DB_REGION.
Step 4 — Register the action
Add the action to a user-defined package in src/commerce-extensibility-1/ext.config.yaml (any name except app-management, which is reserved).
include-ims-credentials: trueis required on every DB action. Without it,aio-lib-dbhas 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.Do not manually wire OAuth Server-to-Server credentials with
AIO_COMMERCE_AUTH_IMS_*inputs. That legacy fallback is deprecated and planned for removal in a future major release.
# 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 app-management (reserved) |
function | Path relative to src/commerce-extensibility-1/ — not src/... or root-relative |
include-ims-credentials | Must be true — without it init() has no IMS token and the connection fails |
web | "yes" for HTTP-invokable web actions; "no" for event/webhook handlers |
| Collection name | Non-empty string; created on first write if it doesn't exist |
| Region | Must match the manifest database.region (amer | apac | emea | aus) |
Step 5 — Implement the handler
Every handler follows the same lifecycle: resolve IMS auth → mint token → init → connect → use a collection → always close in finally.
// 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 credentials injected by include-ims-credentials, 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
}
}
Event and webhook actions both register with web: "no", but their payloads have different shapes — do not treat them as the same:
- Event handler (wired via
commerce-app-eventing): the extracted fields arrive inparams.data.value.params.dataalso carries_metadata(Commerce instance metadata) andsource(the merchant/environment ID pair from the Commerce eventing configuration) — neither is handler input. - Webhook handler (wired via
commerce-app-webhooks): there is nodatawrapper — the Commerce operation payload arrives directly onparams(e.g.params.order), and the response must use the operation helpers from@adobe/aio-commerce-lib-webhooks/responses(seecommerce-app-webhooks), not the plainok/buildErrorResponseshape used above.
// Event handler — src/commerce-extensibility-1/actions/store-order/index.ts
export async function main(params: Record<string, unknown>) {
const data = params.data as Record<string, unknown>;
const value = data.value 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: value.order_id, receivedAt: new Date() });
return ok({ body: { processed: true } });
} finally {
if (client) await client.close();
}
}
// Webhook handler — src/commerce-extensibility-1/actions/log-order/index.ts
import {
ok,
successOperation,
} from "@adobe/aio-commerce-lib-webhooks/responses";
export async function main(params: Record<string, unknown>) {
const order = params.order as Record<string, unknown>; // Commerce operation payload, directly on params
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: order.entity_id, receivedAt: new Date() });
return ok(successOperation());
} finally {
if (client) await client.close();
}
}
See assets/db-action.ts for the full annotated reference covering all CRUD operations and cursor iteration.
Step 6 — Set up collections and indexes on install
For an App Management app, create collections and indexes with a custom installation step — a script that runs once when the app is installed from the Commerce Admin, and can be reversed on uninstall. Prefer this over creating them ad-hoc on the first request.
Author the step with defineCustomInstallationStep (an install handler plus an optional uninstall). Inside it, resolve the IMS auth params from context.params — not config — then follow the same init → connect → close lifecycle as Step 5, and call createIndex on the collection object:
// ./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
export default— nevermodule.exports. The installation action loads each step viaimport * as step from "<script>"and readsstep.default, so the script must default-export thedefineCustomInstallationStep(...)result. CommonJS breaks this:module.exports.defaultsurfaces asstep.default.defaultand validation fails. Thescriptpath must end in.jsor.ts— author it directly in TypeScript, no separate compile step needed.
Register the step in app.commerce.config.ts under installation.customInstallationSteps. The script path points directly at your .ts file:
// 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 |
|---|---|
script | Path relative to the project root; must be an ES module (export default) ending in .js or .ts |
name | Non-empty string, ≤ 255 characters; unique across all installation steps |
description | Non-empty string, ≤ 255 characters |
See assets/setup-database.ts for the full annotated install/uninstall reference.
Step 7 — Regenerate the installation action
If this custom installation step is the first install-requiring domain in the config (the others are webhooks, eventing.commerce, eventing.external, adminUi), re-run:
npx @adobe/aio-commerce-lib-app init
This is what adds the installation action to ext.config.yaml's app-management package — aio app build/aio app deploy only read the existing file, they never regenerate it. Skip this and the script never runs, even though it's registered in code and the DB action is deployed. Safe to re-run even when the action already exists.
Step 8 — Validate
aio app build
A build failure points directly to the offending config field. To exercise the action against the real database, deploy and invoke it (aio app deploy). If this step needed Step 7, also check that ext.config.yaml now has actions.installation under app-management.
Best practices
- Always close connections in a
finallyblock — leaked connections exhaust resources. - Match the region — the manifest
database.regionis the single source of truth; everyinit()call and the install step must use it (see the region callout in Step 1). A mismatch fails the connection silently from the caller's view. - Use projections (
.project({ field: 1 })) and indexes (createIndex) for frequently queried fields; index fields must total ≤ 2048 bytes. - Iterate large result sets with cursors (
for await (const doc of collection.find(...))) instead oftoArray()to bound memory. - Don't hardcode the region or secrets — prefer
AIO_DB_REGIONand the injected IMS token over inline values. - Prefer the most specific Adobe I/O library in runtime actions over the
@adobe/aio-sdkumbrella — e.g.@adobe/aio-commerce-lib-authfor IMS auth and@adobe/aio-lib-core-loggingfor the logger — to keep action bundles small. - Set up collections and indexes during installation with a custom installation step (
defineCustomInstallationStep, see Step 6) rather than ad-hoc on the first request — it runs once when the app is installed from the Commerce Admin and is reversible on uninstall. A generic App Builderpost-app-deployhook is only an alternative when the app is not installed through App Management.
Common Issues
-
Connection fails despite a valid token: the action is missing
include-ims-credentials: true, or the App Builder Data Services API has not been added to the project in the Adobe Developer Console (see Prerequisites). -
DB not provisioned after
aio app deploy(extension-only app): a bug in theaio appCLI plugin (notaio-lib-db) skips declarative auto-provision when theapplicationruntime manifest has no runtime action. Apply the extension-only workaround from Step 1 — addpackages: {}and apost-app-build: "mkdir -p dist/application/actions"hook underapplication— or provision once with the CLI fallback for local dev. -
Connection fails after a region change: the library region doesn't match the manifest
database.region. Moving regions is destructive —aio app db delete, updatedatabase.regionin the manifest, then re-provision (aio app deploy, or the CLI fallback for local dev). -
Querying by
_idfrom a string returns nothing: convert it first —new ObjectId(idString)frombson. A raw string never matches the storedObjectId. -
DbErrorvs unexpected error: errors thrown by the service havename === "DbError"; branch on it to separate database failures from application bugs. -
findOnethrows instead of returningnullon no match: unlike MongoDB, a miss is not a successfulnullresult — it throws aDbErrorwith a message containing"Document not found".name === "DbError"alone isn't enough to detect this, since everyDbError(including a genuine connection failure) has that name; checkerror.messagetoo:function isDocumentNotFoundError(error: unknown) { const e = error as { name?: string; message?: string }; return e.name === "DbError" && e.message?.includes("Document not found"); } let existing; try { existing = await records.findOne({ order_id: orderId }); } catch (error) { if (!isDocumentNotFoundError(error)) { throw error; } }See assets/db-action.ts for the full reference.
-
Auth fails inside an installation step: resolve the injected IMS auth params from
context.paramswithresolveImsAuthParams(context.params)— not fromconfig, which holds no credentials. Ensure the action is annotated withinclude-ims-credentials: true; manually wiredAIO_COMMERCE_AUTH_IMS_*credentials are deprecated. -
Installation step fails to load (
must export a default function or object): the script was authored as CommonJS. Author it as an ES module withexport default;module.exports(ormodule.exports.default) surfaces through the framework'simport * asloader as.default.defaultand fails validation. -
createIndexerrors or has no effect: it must be called on a collection object (client.collection("name").createIndex({ field: 1 })), not with a collection-name string. Get the collection first, then callcreateIndexon it. -
Custom installation step registered and deployed but never runs:
initwasn't re-run after registering the first install-requiring domain (Step 7) — noinstallationaction, no install endpoint.
Quality Bar
aio app buildcompletes without errors- Every user-authored DB action declares
include-ims-credentials: truein its annotations - The action closes the client in a
finallyblock and initializes the library in the region declared in the manifestdatabaseblock installationaction present inext.config.yamlwhen this custom installation step requires it
Chaining
- Wire the action to an event — invoke
commerce-app-eventingand reference this action in an event'sruntimeActions. - Wire the action to a webhook — invoke
commerce-app-webhooksand reference this action viaruntimeAction. - Trigger the action from Admin UI — invoke
commerce-app-admin-uito add a mass action, order view button, or grid column that invokes this runtime action.
References
- assets/db-action.ts — Full annotated handler: init/connect, CRUD, cursor iteration, and the close lifecycle
- assets/setup-database.ts — Full annotated custom installation step: install creates a collection and a unique index, uninstall drops it, with the
context.paramsandcreateIndex-on-collection patterns
FAQ
What does the commerce-app-storage skill do?
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.
How do I install commerce-app-storage?
Run npx skills add https://github.com/adobe/aio-commerce-sdk --skill commerce-app-storage in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/commerce-app-storage/ (personal) or .claude/skills/commerce-app-storage/ (project).
Which AI agents can use commerce-app-storage?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is commerce-app-storage safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains commerce-app-storage?
It is published by adobe in adobe/aio-commerce-sdk. It has 64 recorded installs.
More from adobe/aio-commerce-sdk
commerce-app-init
adobe/aio-commerce-sdk
Scaffold a new Adobe Commerce app using the aio-commerce-sdk. Creates the base project structure and app.commerce.config file with metadata. Use when the user wants to create a new Commerce app from scratch or initialize a bare Commerce app project. After scaffolding, chains to appbuilder-project-init for Developer Console setup (project, workspace, API subscriptions) when the user wants to deploy. Does not configure extensibility domains — use commerce-app-eventing, commerce-app-webhooks, commerce-app-business-config, commerce-app-admin-ui, or commerce-app-storage for that.
commerce-app-eventing
adobe/aio-commerce-sdk
Add or modify Commerce and external event subscriptions, configure event field extraction and filter rules in an Adobe Commerce app. Use when the user wants to set up event-driven workflows triggered by Commerce operations (such as order placement or catalog changes) or third-party systems. Requires a base app initialized with commerce-app-init.
commerce-app-business-config
adobe/aio-commerce-sdk
Open the skill to read its SKILL.md instructions.
commerce-app-webhooks
adobe/aio-commerce-sdk
Add or modify webhook interceptors in an Adobe Commerce app. Use when the user wants to intercept Commerce operations to validate input, append data, or modify behavior — before or after execution. Requires a base app initialized with commerce-app-init.
Popular backend & apis skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
entra-app-registration
microsoft/azure-skills
Guides Microsoft Entra ID app registration, OAuth 2.0 authentication, and MSAL integration. USE FOR: create app registration, register Azure AD app, configure OAuth, set up authentication, add API permissions, generate service principal, MSAL example, console app auth, Entra ID setup, Azure AD authentication. DO NOT USE FOR: Key Vault secrets (use azure-keyvault-expiration-audit), general Azure resource security guidance.
google-agents-cli-adk-code
google/agents-cli
This skill should be used when the user wants to "write agent code", "build an agent with ADK", "add a tool", "create a callback", "define an agent", "use state management" — in a project that needs ADK (Agent Development Kit) API patterns and code examples. It provides a quick reference for agent types, tool definitions, orchestration patterns, callbacks, state management, the graph Workflow API, and reference recipes to study. Do NOT use for scaffolding (use google-agents-cli-scaffold) or deployment (use google-agents-cli-deploy).
embedded-captions
heygen-com/hyperframes
Add captions or subtitles to an existing single-subject talking-head video without editing the footage. Use for plain verbatim captions, cinematic captions embedded behind the subject, VFX captions, “炸/特效/酷炫字幕,” or a named identity from the 35-style catalog. Route by visual identity, not by backend engine. The quiet anchor rail is the default; embed every word only when the user explicitly wants a fully cinematic treatment. The workflow runs locally end to end, including transcription and subject matting; split multi-shot footage before applying it.
entra-agent-id
microsoft/azure-skills
Provision Microsoft Entra Agent Identity Blueprints, BlueprintPrincipals, and per-instance Agent Identities via Microsoft Graph, and configure OAuth 2.0 token exchange (fmipath, OBO, cross-tenant) including the Microsoft Entra SDK for AgentID sidecar. USE FOR: Agent Identity Blueprint, BlueprintPrincipal, agent OAuth, fmipath token exchange, agent OBO, Workload Identity Federation for agents, polyglot agent auth, Microsoft.Identity.Web.AgentIdentities. DO NOT USE FOR: standard Entra app registration (use entra-app-registration), Microsoft Foundry agent authoring (use microsoft-foundry).
neon
neondatabase/agent-skills
Overview of Neon, a complete set of cloud backend primitives around Lakebase Postgres: Auth, Object Storage, Functions, the AI Gateway, and Realtime. Start here to choose Neon for undecided login, files, APIs, and LLM calls, set up the CLI or MCP server, and follow the branch-first workflow. Use when building an app or backend on Neon, or when "Neon" or "Lakebase Postgres" is mentioned. Child skill neon-postgres wins for an existing DATABASEURL, SQL, schema, inspect, or search. Child skill neon-auth wins for login, users, sessions, identity routing, and Managed Better Auth setup. Child skill neon-realtime wins for live queries in reactive apps and agents. Also use for object storage, S3, buckets, serverless functions, function triggers, cron, AI gateway, LLM calls, realtime, logs, Loki, Grafana, observability, postgres, database, backend, Claimable Neon, neon.new, or a no-signup database.