Start with the structural diagnostic:
node ace commerce:doctorThen reproduce the failing request with the same database, configuration, headers, cookies, and payment mode as the application. The generated storefront returns commerce failures as:
{
"error": {
"code": "E_INSUFFICIENT_STOCK",
"message": "Insufficient stock",
"details": {}
}
}Use error.code for program logic. Messages and details are diagnostic context.
Installation and boot
commerce:doctor reports missing tables
Run:
node ace migration:status
node ace migration:run
node ace commerce:doctorIf migrations were already applied under a different tablePrefix, restore the matching configuration. Do not edit applied migration files or change the prefix to hide the error. A prefix change requires a host migration that renames the schema.
Commerce services cannot be resolved
Confirm adonisrc.ts contains the core provider and command entrypoint. Re-run the core configurator in development if they are absent:
node ace configure @adocommercekit/coreReview and commit the changes. Do not run the configurator automatically in production.
Session cart tokens require @adonisjs/session middleware
The generated config uses sessionCartToken(). Install and configure @adonisjs/session, ensure session middleware runs on the commerce routes, and preserve the cookie in the client.
For a stateless storefront, use headerCartToken() in config/commerce.ts. The cart response contains the opaque token; send it back in the configured header. Never accept a cart database ID as the token.
Unknown payment, shipping, or other driver
A configured default or database record references a key that is not registered in config/commerce.ts. Check:
payments.defaultexists inpayments.methods;- the checkout
payment.methodis registered; - shipping methods reference a key in
shipping.calculators; - every driver entry is a factory such as
manualPayment(), not a partial object.
Restart Adonis after changing configuration because drivers are resolved and cached at boot.
Queue integration fails at boot
jobs.dispatcher: 'queue' dynamically loads @adonisjs/queue. Install and configure a compatible version or switch back to inline for a single-process development environment. Run commerce:doctor after the change.
Storefront requests
/api/commerce/* returns 404
The storefront package publishes, rather than mounts, routes. Confirm:
node ace configure @adocommercekit/storefront-apicompleted;start/routes/commerce.tsexists;adonisrc.tspreloads#start/routes/commerce;node ace list:routesincludes names beginning withcommerce..
If you moved or renamed published files, update their imports and preload entry as ordinary Adonis code.
A new cart appears on every request
The client is not returning the cart credential. With the default session strategy, use one cookie jar:
curl -c .cart-cookie -b .cart-cookie http://localhost:3333/api/commerce/cartIn a browser, ensure credentials and cookie attributes permit the request origin. Behind HTTPS or a proxy, check Adonis session cookie security and proxy trust.
With headerCartToken(), read the token from the cart response and return it in the configured header.
Mutating routes fail CSRF validation
Adocommerce Kit does not disable CSRF for ordinary cart or checkout requests. For a browser session, expose Shield’s XSRF cookie and send X-XSRF-TOKEN. For a token-authenticated API, define an explicit alternative CSRF policy in the host application.
Only the external payment webhook path should normally be excluded from CSRF.
Validation rejects an address
countryCode must be a two-letter code. The published validator requires postalCode for countries in its configured list and limits address field lengths. Because the validator is host-owned, update it deliberately if the business supports a different address contract.
Rate limits are inconsistent between instances
The generated limiter is process-local. Each application instance has separate counters. Replace the published middleware with a shared limiter or enforce the same policy at the gateway. See HTTP scaling and rate limits.
Catalog, cart, and checkout errors
| Code | Meaning | Corrective action |
|---|---|---|
E_PRICE_NOT_FOUND |
No applicable price for the variant, channel, currency, customer group, quantity, and current time | Add or correct a price list/price and channel assignment; do not invent a fallback amount in the controller |
E_CART_NOT_ACTIVE |
The cart is checking out, completed, abandoned, or otherwise unavailable | findOrCreateFromContext replaces terminal carts automatically; when passing carts explicitly, create a new one |
E_CART_PRICES_CHANGED |
Price snapshots changed between review and checkout | Show the returned diff, refresh totals, and ask the customer to confirm before retrying with a new idempotency key |
E_LINE_QUANTITY |
Quantity is outside service limits | Keep API validation and carts.guardMaxLineQuantity aligned |
E_INSUFFICIENT_STOCK |
Allocation cannot satisfy the requested quantity | Refresh availability, reduce quantity, or apply the intended backorder policy |
E_SHIPPING_NOT_ELIGIBLE |
The selected method no longer matches the cart/address | Re-list shipping methods and require a new selection |
E_STATE_TRANSITION |
A stale or invalid domain transition was attempted | Reload the record and apply an allowed transition; do not update state columns directly |
E_PAYMENT_DECLINED |
Provider rejected the payment | Use details.declineCode to choose customer copy; do not retry fraud or authentication failures blindly |
E_PAYMENT_PROVIDER |
Provider request or response failed | Correlate logs with the provider, preserve idempotency, and retry only when provider semantics allow it |
E_IDEMPOTENCY_CONFLICT |
One key was reused with a different request payload | Generate a new key for the changed intent; never mutate and replay the old request |
E_WORKFLOW_FAILED |
Checkout failed and compensation ran | Inspect the failing step and any compensationErrors; reconcile stock and payment state before manual replay |
Stripe and webhooks
Stripe checkout returns payment_not_authorized
The PaymentIntent did not reach authorized or captured. Inspect its provider state and Stripe logs. Common causes are a missing/invalid PaymentMethod ID, a decline, or requires_action.
The generated storefront controller does not complete 3DS/SCA. See Strong Customer Authentication.
E_WEBHOOK_SIGNATURE
Check all of the following:
STRIPE_WEBHOOK_SECRETbelongs to this exact endpoint and mode;- local Stripe CLI forwarding uses the secret printed by the current
stripe listenprocess; - the proxy and middleware preserve the raw body bytes;
- the
Stripe-Signatureheader reaches Adonis unchanged; - server time is synchronized and the event is within the configured tolerance.
Do not log the webhook secret or bypass verification to make a delivery succeed.
Stripe delivers repeatedly even though the endpoint returns 200
Stripe retries can overlap and Adocommerce Kit deduplicates on provider event ID. Confirm the webhook-event row is recorded and inspect whether it is handled, unhandled, or failed. A verified unknown event intentionally returns 200; add support only if that event should change domain state.
Deferred payments and regional providers
An order stays in pending_payment forever
Deferred providers settle from a provider notification or a scheduled expiry sweep, not from the checkout response. Confirm that jobs.dispatcher is 'queue' with a running worker, that the expiry job is scheduled, and that the provider’s webhook actually reaches the application. commerce:doctor fails in production when a deferred driver is registered without the queue dispatcher.
commerce:doctor reports a deferred driver without retrieveIntent
Every provider whose capabilities().flow is deferred must implement retrieveIntent so status can be re-queried independently of webhook delivery. A custom driver that reports deferred without it will fail doctor by design; see authoring a driver.
Midtrans notifications are rejected or settle the wrong order
Midtrans signatures use its exact SHA-512 formula over order ID, status code, gross amount, and server key. A mismatch usually means the wrong server key, the wrong environment, or a re-serialized gross amount. After verifying, the provider re-queries current transaction status before returning a normalized event, so a stale or spoofed notification body cannot settle an order on its own.
Xendit callbacks fail verification
Xendit does not use Midtrans signature rules. It authenticates callbacks with a constant-time x-callback-token comparison and its API with Basic secret-key auth. Confirm the token matches the configured endpoint and that the pinned 2024-11-11 Payment Requests version is still current for your account.
A refund is rejected as manual-only
MidtransManualRefundRequiredError and KodeUnikManualRefundRequiredError are expected for channels without automatic refunds. Midtrans offers automatic refunds only for QRIS and GoPay; every other channel, and every kode-unik payment, requires an operator-recorded manual transfer reference. Check MIDTRANS_REFUND_CAPABILITIES before offering a refund in the UI.
Kode-unik allocation throws a RangeError
Visible codes run 001–999 per receiving account, so at most 999 leases can be active at once. Exhaustion raises All kode-unik values are active for receiving account <id>. Shorten the lease window (expiresAfterMinutes, default 1440), add receiving accounts, or move high volume to a gateway. Allocating twice for the same reference returns the existing lease rather than a second code.
Biteship tracking webhooks always fail
Biteship verification fails closed: without a host-supplied webhookVerifier and a signature header, the request is rejected. Supply a verifier that matches your pinned Biteship contract instead of disabling verification.
Shipping quotes are missing, stale, or rejected at checkout
Quote identity covers parcel values, weight, dimensions, destination, COD, insurance, currency, and provider options, and the rate cache TTL is capped at 60 seconds. A changed cart therefore invalidates a previous quote, and checkout rejects a selection whose signed quote token no longer matches. Re-list quotes and require a fresh selection. RajaOngkir reports waybill creation, fulfillment, COD, and insurance as unsupported rather than emulating them, so a host that needs those must use Biteship.
Indonesian address or region lookups return nothing
No region rows ship with @adocommercekit/id. Import a licensed source with commerce:id:regions-import --file. The importer rejects a non-HTTPS source or license URL and any expectedChecksum that is not a 64-character SHA-256 digest.
WhatsApp messages are never delivered
Sends require a pinned Graph API version, an approved template, and current purpose consent checked both before enqueue and before send; default configuration is disabled. WhatsAppTemplateNotApprovedError and retry exhaustion call only a host-configured fallback notifier, and there is no assumed email fallback. Missing consent is a correct refusal to send, not a bug — see the PDP operations runbook.
COD remittance rows do not settle
commerce:id:cod-import is idempotent and never guesses. It reports settled, duplicates, unmatched, ambiguous, and invalid counts with one warning per problem row. Rows that do not match exactly one order must be reconciled by hand; re-running the same file settles nothing twice.
Admin panel
The panel throws you back to the login route every few clicks
commerceAdminAuth redirects a page load to redirectTo whenever the session
carries no authenticated actor. Mid-session that almost always means the browser
handed back a session id the app never issued, because cookies are scoped by
host and path but not by port. Every AdonisJS app reachable on the same
hostname writes adonis-session at Path=/ by default, so a second copy of your
app on another port, or an unrelated project on localhost, silently replaces
the panel’s session id. The panel cannot decrypt it, mints a fresh empty session,
and the next click is a redirect to the login route.
Give the app its own session cookie name in config/session.ts — keying it on
the listen port gives every local instance a distinct one:
cookieName: `myapp_session_${env.get('PORT')}`,Shield’s XSRF-TOKEN cookie name is not configurable, so a co-located app can
still cost you one E_BAD_CSRF_TOKEN on the next mutation. The panel re-issues
the token on every response, so a reload clears it.
Panel mutations lose their flash notice, or every request answers 431
SESSION_DRIVER=cookie serialises the whole session into one cookie, and the
panel’s flash payload is unbounded: a validation failure on a 200-line shipment
renders a 35KB Set-Cookie. Browsers discard any cookie past 4096 bytes, so that
response’s session write — flash included — is dropped, and a client permissive
enough to store it sends a header the server answers with
431 Request Header Fields Too Large until the cookie expires. The cookie store
also names the data cookie after the session id, so every id change strands the
previous cookie in the browser for good. Run the panel on database or redis.
Generated-file upgrades
commerce:upgrade reports a conflict
The host file differs from its recorded publication base. Adocommerce Kit leaves it untouched and stages incoming content plus merge instructions under .commercekit/incoming.
Merge deliberately:
- compare host, base, and incoming versions;
- preserve application-specific behavior and adopt relevant upstream security or contract changes;
- run typecheck and the affected HTTP smoke;
- update and commit
.commercekit/publicationswith the resolved host file.
Do not delete .commercekit/publications; without the base, future upgrades cannot distinguish a safe update from a host modification.
Generated API docs do not match exports
The API page is generated from TypeScript entrypoints. From the workspace root run:
pnpm docs:buildDo not edit docs/src/content/docs/api/generated.mdx by hand.
Still unresolved
Capture these facts before opening an issue:
- exact package versions and package manager lockfile;
- Node.js, AdonisJS, Lucid, database, and queue versions;
- database dialect;
commerce:doctoroutput;- commerce error code and sanitized details;
- minimal configuration and reproduction;
- whether published files were modified;
- sanitized provider request/event identifiers.
Report issues through the GitHub issue tracker without secrets, payment tokens, card data, or customer PII.