If you’ve spent years writing Magento modules, ACCS can feel like someone took away your toolbox and handed you a very nice remote control. The first time you open a ticket that says “just add a plugin on the quote totals collector” and the honest answer is “you can’t”, it stings. This post is about that gap: ACCS vs PaaS custom modules, what Adobe Commerce as a Cloud Service won’t let you do, and what you build instead.
It’s the fifth post in my ACCS series. If you’re new to it, read the introduction to ACCS first, then ACCS vs Adobe Commerce on Cloud. If you’ve already decided to move, the PaaS to ACCS migration checklist covers the project plan. This one is narrower. It’s about code.
Facts about ACCS below come from Adobe’s SaaS vs PaaS feature comparison and the developer docs on developer.adobe.com/commerce/extensibility. The code samples are mine, they’re illustrative, and I haven’t run them against a live ACCS tenant. Where I’m guessing, I’ll say so.
The short version
On PaaS (Adobe Commerce on cloud infrastructure), the codebase is yours. You composer require things, drop a module in app/code, write plugins and observers, and ship it all through your deployment pipeline. On ACCS, Adobe owns the core and the infrastructure, and the feature comparison says extensibility is “out-of-process only (APIs, events, App Builder)”. Your code runs somewhere else and talks to Commerce through supported contracts.
That one sentence explains almost every limit in this post. A versionless platform can only stay versionless if nobody is hooking into its internals. Annoying, yes. Also consistent.
ACCS vs PaaS custom modules: what you can’t do
You can’t ship PHP at all
No custom modules, no plugins, no observers, no di.xml preferences. Adobe’s comparison lists the technologies as CSS, CLI, HTML, JS and Node for ACCS, against CSS, CLI, HTML, JS, PHP and XML for PaaS. Even modules that Adobe itself ships work differently: on PaaS they need a composer install, on SaaS they’re pre-installed.
This also rules out most third-party extensions from the Marketplace, at least the PHP ones. If a vendor hasn’t built an out-of-process version, you can’t install theirs on ACCS. Check the extension list in your project before you sign anything.
You can’t invent your own data model
PaaS lets you create tables, EAV attributes, custom entities, whatever the project needs. ACCS gives you custom attributes on core and B2B entities. That covers a lot, honestly. A “warranty registration” entity with its own lifecycle and its own grid is a different story. Adobe’s own footnote says industry-specific models, such as dealer-specific attributes, “could require additional architectural considerations”.
Persistent storage is the other half. The custom storage row reads “DB, file, cache, queue” for PaaS and “App Builder state library (file only)” for ACCS, with database storage for App Builder still in early access when I last checked. The footnote adds that long-term storage needs may mean provisioning your own infrastructure. So plan on a separate datastore for anything serious.
You can’t customise search, email types or the Admin the old way
- Search index: native search customisation on PaaS, “requires third-party solutions” on ACCS.
- Emails: full customisation on PaaS, standard email templates only on ACCS. A new email type is out.
- Admin: PaaS allows PHP customisation and theming. ACCS allows App Builder extensibility only, which in practice means the Admin UI SDK and single-page apps embedded in the Admin.
- Storefront: there’s no Luma on ACCS. Per Adobe’s compatibility notes, you use the Edge Delivery Services storefront.
You can’t pick any webhook or event you like
This one catches people out because webhooks and events do exist on ACCS. They’re just curated. On PaaS you can define a webhook in a webhooks.xml inside your module, targeting an observer. or plugin. method. On ACCS, you subscribe from a predefined list, in the Admin under System > Webhooks or through REST. Adobe’s webhook docs say plainly that ACCS doesn’t support every possible webhook method, and that you open a support ticket to ask for more.
Even the small PaaS escape hatches go away. Here’s my favourite example. A webhook response can add or replace data using an instance class, but only certain data object types are allowed. On PaaS you can allow more types by adding an allowedTypes argument in your own module’s di.xml. On ACCS there is no module to put that in. Same story with authentication: PaaS can write a PHP header resolver class, while ACCS gives you an OAuth credential panel in the Admin.
You can’t reach the server
I couldn’t find ACCS documented with SSH, bin/magento or direct database access, and Adobe’s feature comparison doesn’t list them. On a multi-tenant SaaS that’s what I’d expect. I’d plan as if you won’t have any of it and confirm with your Adobe contact if a project depends on it.
ACCS vs PaaS custom modules at a glance
Here’s the cheat sheet I wish I’d had during my first ACCS discovery call. The right-hand column is a mix of what Adobe documents and what I’d reach for first, so treat it as a starting point.
| What a PaaS custom module does | ACCS limitation | What to do instead |
|---|---|---|
| PHP plugins, observers, preferences | Not possible. Out-of-process only | App Builder actions triggered by Adobe I/O Events or webhooks |
| Custom tables and entities | Custom attributes on core and B2B entities only | Attributes plus an external datastore; App Builder state or DB storage (early access) |
webhooks.xml for any method | Predefined list, set up in Admin or REST; ticket for new methods | Use the closest supported method, and ask support early |
| PHP header resolvers for hook auth | OAuth credential config in the Admin | Developer Console OAuth credential |
| Custom Admin pages with UI components | App Builder only | Admin UI SDK single-page apps |
crontab.xml jobs and queue consumers | No module to hold them | Runtime actions, events, and alarms (check current support) |
| Search index customisation | Third-party solutions | Live Search for standard needs, external search if not |
| New email types | Standard templates only | Send through your own email service from an App Builder action |
| Custom GraphQL and REST endpoints | API Mesh with custom resolvers | API Mesh |
| Custom payment, shipping, tax logic | No PHP carriers or collectors | Out-of-process checkout starter kit apps |
| Luma or custom PHP theme | Luma not available | Commerce Storefront on Edge Delivery Services |
| Marketplace PHP extensions | Can’t composer install | Vendor’s out-of-process version, or a native feature |
What to do instead: three patterns I’d start with
Adobe’s App Development Comparison maps the old Magento concepts to App Builder ones. Models and controllers become actions. Plugins and observers become I/O Events. Cron jobs become alarms. Composer becomes npm. Once you have that translation table in your head, the move feels less alien.
Pattern 1: a webhook for logic that has to block
Webhooks are the closest thing to a synchronous plugin. Commerce calls your endpoint, waits, and acts on the answer. The response format supports success, exception, add, replace and remove operations. Here’s a stock check on add to cart, as an App Builder action. Illustrative only. The payload shape depends on the webhook method and the fields you map, so look at a real request in the webhook logs before trusting my property paths.
// actions/validate-stock/index.js (illustrative)
const { Core } = require('@adobe/aio-sdk');
const reply = (body) => ({ statusCode: 200, body });
async function main(params) {
const logger = Core.Logger('validate-stock', { level: params.LOG_LEVEL || 'info' });
const sku = params.data?.product?.sku;
if (!sku) return reply({ op: 'success' }); // nothing to check
try {
const res = await fetch(`${params.ERP_URL}/stock/${encodeURIComponent(sku)}`, {
headers: { Authorization: `Bearer ${params.ERP_TOKEN}` },
signal: AbortSignal.timeout(1500)
});
const { available } = await res.json();
if (!available) {
return reply({ op: 'exception', message: 'Sorry, that item just went out of stock.' });
}
return reply({ op: 'success' });
} catch (e) {
logger.error(`Stock lookup failed for ${sku}: ${e.message}`);
return reply({ op: 'success' }); // fail open; decide this per business rule
}
}
exports.main = main;Two opinions. First, keep these fast. Each hook has hard and soft timeouts you can set, and your shopper is waiting on the answer. Second, decide deliberately whether a failure should fail open or closed. I fail open for stock checks and closed for anything payment-related, but your merchant may disagree. Adobe also advises creating a webhook only when you truly need a synchronous answer. For everything else, use events.
Pattern 2: an event for everything that can wait
Most observers I’ve written for real projects didn’t need to block anything. They pushed an order somewhere, flagged a customer, or kicked off an export. Those translate neatly to Adobe I/O Events for Adobe Commerce. This is what the PaaS version usually looks like:
// Observer/PushOrderToOms.php (the PaaS way)
public function execute(Observer $observer): void
{
$order = $observer->getEvent()->getOrder();
$this->omsClient->push($order->getIncrementId(), (float) $order->getGrandTotal());
}And here’s a rough ACCS equivalent, an action subscribed to the order event. Again illustrative, and check the actual event payload in your own registration.
// actions/order-created/index.js (illustrative)
const { Core } = require('@adobe/aio-sdk');
async function main(params) {
const logger = Core.Logger('order-created', { level: params.LOG_LEVEL || 'info' });
const order = params.data?.value;
if (!order?.increment_id) {
return { statusCode: 400, body: { error: 'No order in event payload' } };
}
const res = await fetch(`${params.OMS_URL}/orders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': params.OMS_KEY },
body: JSON.stringify({ ref: order.increment_id, total: order.grand_total })
});
if (!res.ok) {
// let the failure surface so the event can be investigated and replayed
logger.error(`OMS returned ${res.status} for ${order.increment_id}`);
return { statusCode: 502, body: { error: 'OMS push failed' } };
}
return { statusCode: 200, body: { pushed: order.increment_id } };
}
exports.main = main;Notice what you lost and what you gained. You lost the database transaction. If the OMS call fails, nothing rolls back the order, because the order already exists. You gained isolation. A slow ERP can’t hold up checkout any more, and your code deploys without touching the store.
Pattern 3: API Mesh, the Admin UI SDK and the checkout kits for the rest
Custom API shapes go through API Mesh with custom resolvers. That’s the ACCS row in Adobe’s comparison, and it’s a decent fit for a headless storefront that wants one merged GraphQL endpoint. Admin screens become Admin UI SDK apps. Payment, shipping and tax logic have checkout starter kit apps, one per domain, plus a totals collector example for discounts.
The storefront side deserves its own read. I covered it in Edge Delivery Services Storefront for ACCS.
What still belongs in PHP (on PaaS)
Don’t read any of this as “App Builder replaces everything”. It doesn’t. Adobe’s Experience League write-up on replacing PHP with App Builder where it matters describes a project where roughly 70% of the functionality moved to App Builder and roughly 30% stayed as native PHP. What stayed was checkout and order placement adjustments, pricing, cart and quote logic, and latency-sensitive synchronous hooks.
That 30% is exactly what ACCS can’t host. If your roadmap leans on deep checkout or pricing changes in PHP, say so in your platform discussion. It might be the thing that keeps you on PaaS, which is a perfectly respectable outcome. And if you’re staying on PaaS for now, start moving the other 70% out-of-process anyway. Adobe recommends that even on PaaS.
A checklist before you commit
- ☐ List every custom module and mark it: replaced natively, can go out-of-process, or in-process only.
- ☐ Count the custom tables and entities. Anything beyond attributes needs an external datastore.
- ☐ Check each webhook and event you’d need against ACCS’s supported list before you design around it.
- ☐ Confirm which Marketplace extensions have an out-of-process version.
- ☐ Find the synchronous logic (checkout, pricing, totals) and decide where it goes.
- ☐ Check that your team has Node and JavaScript skills, not only PHP.
- ☐ Ask Adobe about shell and database access if any process still relies on them.
- ☐ Prototype one real observer as an App Builder action before you estimate the project.
That last one is the best advice here. A single working action tells you more about effort, latency and debugging pain than a week of slides.
FAQ
Can I install Marketplace extensions on ACCS?
Not PHP ones. PaaS modules are installed with composer, and on ACCS they’re pre-installed by Adobe, so you can’t add your own. Ask the vendor whether an App Builder version exists.
Do webhooks replace plugins?
Partly. A webhook can validate, modify or reject, and it runs synchronously. But it only fires on supported methods, and it works on the data in the payload. You can’t intercept any public method in the codebase like you can with a PHP plugin.
Is there an ACCS equivalent of cron?
Adobe’s comparison guide points at I/O Runtime alarms for scheduled work. Because that platform evolves quickly, check the current App Builder docs before you design around alarms.
Can I store custom data in ACCS?
Only through the App Builder state library today (file-based), with database storage in early access when I last read the docs. For anything long-lived or relational, plan to run your own store.
Is ACCS “worse” because I can’t write modules?
No. It’s a different trade. You give up depth and get automatic upgrades and less to maintain. Whether that’s worth it depends on how much of your logic lives in that deep 30%.
My take
I used to treat custom modules as the answer to everything. Honestly, a lot of them were just integrations wearing a Magento costume, and those move to App Builder without much pain. The painful ones are the ones that bend checkout or pricing from the inside. Those are the modules you need to talk about on day one, not discover in month four.
If you’re weighing the platforms, start with the full ACCS vs Cloud comparison, then run your module audit with the migration checklist. And if you’ve hit an ACCS limit I haven’t covered, tell me in the comments. I’m collecting them.




