commerce-app-storage
Integrates App Builder Database Storage into an existing Adobe Commerce app, scaffolding runtime actions that read and write documents using MongoDB-like…
Identity proven by GitHub's verified organisation badge (adobe)
- Agents
- Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode
- License
- Apache-2.0 (permissive)
- Pin
- 940b879pinned Oct 9, 2026
- Context cost
- about 5,300 tokenswhen the skill loads
- GitHub stars
- 197on adobe/skills
- Scripts
- 2 scripts
What it does
“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.”
Generated summary from the skill's own text
Integrates App Builder Database Storage into an existing Adobe Commerce app, scaffolding runtime actions that read and write documents using MongoDB-like collections. Handles both web actions (HTTP-invokable) and event/webhook handlers, with support for custom installation steps to set up collections and indexes.
When to use it
- Building persistent storage for Commerce app data with queryable documents
- Creating web actions or event handlers that read and write to collections
- Setting up indexes and collections during app installation
- Storing order, customer, or transaction data in a Commerce extension
When not to
- App has not been initialized with commerce-app-init first
- App Builder Data Services API is not added to the Adobe Developer Console
- Storing data outside MongoDB-like document model fits better
Install
These commands install from the publisher's repository and need no account.
skills CLI
npx skills add adobe/skills --skill commerce-app-storageInstalls from the repository's default branch, which may have changed since the commit we checked.
GitHub CLI 2.90 or later
gh skill install adobe/skills commerce-app-storage@940b8795c0df3e25de68ce3881dc90855129b215Installs the commit we checked (940b879).
Copy by hand
Download the folder at 940b879 and put it in .claude/skills/commerce-app-storage for Claude Code and in .agents/skills/commerce-app-storage for Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode.
Claude Code plugin
/plugin install adobe-for-creativity@claude-plugins-officialThe publisher's plugin in Claude's official marketplace. It may hold more than this skill, and we have not pinned it.
Copy for your agent
A prompt for any coding agent. It installs this skill from GitHub at the pinned commit, copies the files as they are, and runs nothing unless you ask.
Prompt
Install the agent skill "commerce-app-storage" by Adobe, Inc.. Source, pinned to commit 940b8795c0df3e25de68ce3881dc90855129b215: https://github.com/adobe/skills/tree/940b8795c0df3e25de68ce3881dc90855129b215/plugins/commerce/app-management/skills/commerce-app-storage 1. Download that folder at exactly this commit. Do not use the default branch. 2. Copy every file unchanged, keeping the folder structure, into .claude/skills/commerce-app-storage/ for Claude Code and .agents/skills/commerce-app-storage/ for Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode. Create the folders if they do not exist. 3. Do not edit the files, and do not run any script in the folder unless I ask you to. 4. When you are done, list the files you copied and where you put them. Listing and checks: https://www.agentnative.dev/skills/adobe-inc/commerce-app-storage
SKILL.md at the pin
---
name: commerce-app-storage
description: >
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.
license: Apache-2.0
compatibility: >
Requires Node.js 22+, aio CLI, @adobe/aio-commerce-lib-app, and a provisioned
workspace database with the App Builder Data Services API added to the project
in the Adobe Developer Console. Requires a base app initialized with commerce-app-init.
metadata:
author: adobe
sdk-package: "@adobe/aio-commerce-lib-app"
version: "0.0.3"
---
# 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 the `responses` helpers from `@adobe/aio-commerce-lib-core`.
- **Event/webhook action** — invoked by a Commerce event or webhook; referenced from `app.commerce.config.ts` via `commerce-app-eventing` (`runtimeActions`) or `commerce-app-webhooks` (`runtimeAction`).
## Prerequisites
- Verify the app is **scaffolded and initialized**, not merely that the config exists. Require **both**:
- `app.commerce.config.ts` present in the project root, **and**
- the project initialized — signalled by the generated `src/commerce-extensibility-1/` directory and installed `node_modules` (the `@adobe/aio-commerce-lib-app` dependency).
- If `app.commerce.config.ts` is **missing**, stop and invoke `commerce-app-init` first (it writes the config, then runs init).
- If the config is **present but the project is not initialized** (no `src/commerce-extensibility-1/` or `node_modules`), run `npx @adobe/aio-commerce-lib-app init` before 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` + root `tsconfig.json`) that `init` scaffolds for a TypeScript Commerce config — see `commerce-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`:
```yaml
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:
```yaml
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](https://github.com/adobe/aio-cli-plugin-app-storage) is installed:
```sh
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 `region` in the manifest `database` block must match the `region` passed to every `initDb({ region })` call (or `AIO_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`, update `database.region` in the manifest, then re-provision.
## Step 2 — Install the library
```sh
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 to `init()` or set `AIO_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: true` is required on every DB action.** Without it, `aio-lib-db` has 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.
```yaml
# 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`**.
```ts
// 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
}
}
```
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 in `params.data.value`. `params.data` also carries `_metadata` (Commerce instance metadata) and `source` (the merchant/environment ID pair from the Commerce eventing configuration) — neither is handler input.
- **Webhook handler** (wired via `commerce-app-webhooks`): there is no `data` wrapper — the Commerce operation payload arrives directly on `params` (e.g. `params.order`), and the response must use the operation helpers from `@adobe/aio-commerce-lib-webhooks/responses` (see `commerce-app-webhooks`), not the plain `ok`/`buildErrorResponse` shape used above.
```ts
// 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();
}
}
```
```ts
// 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](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**:
```ts
// ./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` — never `module.exports`.** The installation action loads each step via `import * as step from "<script>"` and reads `step.default`, so the script must default-export the `defineCustomInstallationStep(...)` result. CommonJS breaks this: `module.exports.default` surfaces as `step.default.default` and validation fails. The `script` path must end in `.js` or `.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:
```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 |
| ------------- | --------------------------------------------------------------------------------------------------- |
| `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](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:
```sh
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
```sh
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 `finally` block — leaked connections exhaust resources.
- **Match the region** — the manifest `database.region` is the single source of truth; every `init()` 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 of `toArray()` to bound memory.
- **Don't hardcode the region or secrets** — prefer `AIO_DB_REGION` and the injected IMS token over inline values.
- **Prefer the most specific Adobe I/O library** in runtime actions over the `@adobe/aio-sdk` umbrella — e.g. `@adobe/aio-commerce-lib-auth` for IMS auth and `@adobe/aio-lib-core-logging` for 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 Builder `post-app-deploy` hook 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 the `aio app` CLI plugin (not `aio-lib-db`) skips declarative auto-provision when the `application` runtime manifest has no runtime action. Apply the extension-only workaround from Step 1 — add `packages: {}` and a `post-app-build: "mkdir -p dist/application/actions"` hook under `application` — 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`, update `database.region` in the manifest, then re-provision (`aio app deploy`, or the CLI fallback for local dev).
- **Querying by `_id` from a string returns nothing**: convert it first — `new ObjectId(idString)` from `bson`. A raw string never matches the stored `ObjectId`.
- **`DbError` vs unexpected error**: errors thrown by the service have `name === "DbError"`; branch on it to separate database failures from application bugs.
- **`findOne` throws instead of returning `null` on no match**: unlike MongoDB, a miss is not a successful `null` result — it throws a `DbError` with a message containing `"Document not found"`. `name === "DbError"` alone isn't enough to detect this, since every `DbError` (including a genuine connection failure) has that name; check `error.message` too:
```ts
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](assets/db-action.ts) for the full reference.
- **Auth fails inside an installation step**: resolve the IMS auth params from `context.params` (`resolveImsAuthParams(context.params)`) — which carries the injected `AIO_COMMERCE_AUTH_IMS_*` credentials — not from `config`, which holds no credentials. Use `@adobe/aio-commerce-lib-auth`, not `@adobe/aio-lib-core-auth`: the latter's `generateAccessToken` expects `clientId`/`clientSecret` directly and cannot consume the injected params.
- **Installation step fails to load (`must export a default function or object`)**: the script was authored as CommonJS. Author it as an ES module with `export default`; `module.exports` (or `module.exports.default`) surfaces through the framework's `import * as` loader as `.default.default` and fails validation.
- **`createIndex` errors 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 call `createIndex` on it.
- **Custom installation step registered and deployed but never runs**: `init` wasn't re-run after registering the first install-requiring domain (Step 7) — no `installation` action, no install endpoint.
## Quality Bar
- `aio app build` completes without errors
- Every user-authored DB action declares `include-ims-credentials: true` in its annotations
- The action closes the client in a `finally` block and initializes the library in the region declared in the manifest `database` block
- `installation` action present in `ext.config.yaml` when this custom installation step requires it
## Chaining
- **Wire the action to an event** — invoke `commerce-app-eventing` and reference this action in an event's `runtimeActions`.
- **Wire the action to a webhook** — invoke `commerce-app-webhooks` and reference this action via `runtimeAction`.
- **Trigger the action from Admin UI** — invoke `commerce-app-admin-ui` to add a mass action, order view button, or grid column that invokes this runtime action.
## References
- [assets/db-action.ts](assets/db-action.ts) — Full annotated handler: init/connect, CRUD, cursor iteration, and the close lifecycle
- [assets/setup-database.ts](assets/setup-database.ts) — Full annotated custom installation step: install creates a collection and a unique index, uninstall drops it, with the `context.params` and `createIndex`-on-collection patterns
From adobe/skills at 940b879, shown as plain text under its license: Apache-2.0 (permissive). The license text is in plugins/commerce/app-management/skills/commerce-app-storage/SKILL.md.
View SKILL.md on GitHub at 940b879Files
- SKILL.md22 KB · c543f5275890
- assets/
- db-action.ts4.7 KB · 828f6f24d537
- setup-database.ts4.1 KB · 38e4be1be2db
- evals/
- evals.json7.6 KB · aeb501647351
4 files, 38 KB in total. Each file has a sha256 in the pin's manifest; the first 12 characters are shown.
Skill folder on GitHub at 940b879More from Adobe, Inc.
adobe
This skill provides an onboarding tour of the Adobe for creativity connector, displaying available capabilities, example workflows, and which Adobe product…
Claude Code, Codex, Cursor +4 · about 2.2K tokens · pinned 6 days ago
adobe-anydocx
This skill routes DOCX conversion tasks that have a PDF source to Adobe's pdf_export tool with the appropriate format setting.
Claude Code, Codex, Cursor +4 · about 680 tokens · pinned 6 days ago
adobe-anyexcel
This skill routes PDF-to-Excel conversion tasks to the Adobe pdf_export tool with xlsx target format.
Claude Code, Codex, Cursor +4 · about 690 tokens · pinned 6 days ago
adobe-anypdf
This skill routes PDF tasks to Adobe Acrobat tools for higher fidelity than local Python libraries.
Claude Code, Codex, Cursor +4 · about 2.1K tokens · pinned 6 days ago
adobe-anypptx
This skill routes PowerPoint conversion tasks that have a PDF source to Adobe's pdf_export tool with PPTX output format.
Claude Code, Codex, Cursor +4 · about 670 tokens · pinned 6 days ago
adobe-batch-edit-photos
This skill applies consistent visual adjustments to a set of photos so they appear edited together as a cohesive collection.
Claude Code · about 9.9K tokens · pinned 6 days ago
Related in Backend and APIs
nvflare-orient
Routes NVFLARE requests to appropriate downstream skills based on user intent and preliminary source inspection findings.
by NVIDIAOfficial publisher
Claude Code, Codex, Cursor +4 · about 600 tokens · pinned 6 days ago
cloudflare-email-service
This skill enables agents to send transactional emails and route incoming emails using Cloudflare's Email Service platform.
by CloudflareOfficial publisher
Claude Code, Codex, Cursor +4 · about 1.6K tokens · pinned 6 days ago
durable-objects
Build and debug Cloudflare Durable Objects applications for stateful coordination and persistent storage on the edge.
by CloudflareOfficial publisher
Claude Code, Codex, Cursor +4 · about 1.5K tokens · pinned 6 days ago
workers-best-practices
This skill guides code review and development of Cloudflare Workers by applying best practices for production readiness.
by CloudflareOfficial publisher
Claude Code, Codex, Cursor +4 · about 1.3K tokens · pinned 6 days ago
supabase
This skill enables the agent to work with Supabase products including Database, Auth, Edge Functions, Realtime, Storage, and Vectors.
by SupabaseOfficial publisher
Claude Code, Codex, Cursor +4 · about 3.3K tokens · pinned 6 days ago
apple
This skill emulates Apple's OAuth and OIDC services for local development and testing.
by VercelOfficial publisher
Claude Code, Codex, Cursor +4 · about 1.9K tokens · pinned 6 days ago