A Simpler Alternative to Shopify App Development
Shopify app development means building and operating a separate web application: an OAuth install flow, per-shop access tokens you store and refresh, a server you keep running, webhooks you verify and reconcile, and — for a public listing — App Store review. On ShopsWired the equivalent unit is a plugin: a directory containing a manifest.json and some JavaScript files that you push to a store, where the platform runs them next to the data. There is no OAuth, no hosting bill, and no round trip back to infrastructure you own.
That is the architectural difference in one sentence. Below is what it actually changes day to day, and where the Shopify model is still the better choice.
What Shopify app development involves
A Shopify app is, structurally, a third-party web app that talks to a store over the network. Even a minimal one has to handle:
- The install and auth flow — OAuth 2.0, requested scopes, an access token per shop that you persist and protect.
- Hosting — a server or serverless deployment that stays up, because the store calls you and your webhooks arrive whether or not you are ready for them.
- Webhook plumbing — HMAC signature verification, idempotency for redelivery, and reconciliation jobs for the events you missed while you were down.
- Billing — integrating the platform's billing API if you want to charge.
- Review — listing publicly on the App Store means a review process. A custom app built for a single merchant skips the listing, but it still needs the OAuth flow and the hosting.
To be fair to Shopify: this design buys real things. You can write the app in any language, pull in any dependency, keep your own database, and serve thousands of merchants from one deployment you control. Shopify Functions and checkout UI extensions also let some logic run on Shopify's own infrastructure rather than yours, which genuinely removes the round trip for discounts and checkout customization. But outside those specific extension points, you are back to running a web app.
What a ShopsWired plugin is instead
A plugin is a directory. The manifest names the scripts and how each one is wired up; hook names are auto-detected from each script's module.exports, so there is no registration step.
{
"id": "my_plugin",
"name": "My Plugin",
"version": "1.0.0",
"scripts": [
{ "path": "hooks.js" },
{ "path": "cron.js", "schedule": "* * * * *" }
],
"settings": [
{ "key": "api_key", "type": "text", "label": "API Key" }
]
}The scripts run on ShopScript, ShopsWired's server-side JavaScript runtime. You write modern JavaScript (ES2015+), and every handler's first argument is ctx.
Webhooks become hooks — and hooks can say no
This is the difference developers notice first. A webhook is a notification after the fact: by the time it reaches your server, the write already happened, and all you can do is compensate. A ShopsWired data hook runs during the write. Every built-in entity fires before_save, after_save, before_delete and after_delete — product.*, order.*, customer.*, coupon.*, plus record.<type>.* for your own record types.
module.exports = {
"product.before_save": function (ctx) {
if (!ctx.data.sku) {
ctx.data.sku = "AUTO-" + Date.now();
}
// To abort the save:
// throw { error: "Missing SKU" };
}
};Mutating ctx.data in a before_save persists. Throwing rejects the write. And every writer fires them — a merchant editing in the admin, an API call, another plugin's sw.products.save — so there is no back door your validation misses. ctx.old_data carries the previous state (absent on create), which is the diff you would otherwise be reconstructing from your own mirror of the catalog.
Because the hook is on the write path, it runs under a budget: data and event hooks get 5 seconds, checkout.* hooks 10, payment.* hooks 20, route handlers and dashboard widgets 30, and background or scheduled scripts 10 minutes. Anything slow moves off the request with sw.task.bg, a durable fire-and-forget task that is persisted before it runs:
module.exports = {
"order.after_save": function (ctx) {
sw.task.bg((ctx) => {
const order = sw.orders.get(ctx.args.orderId);
// slow work: call an external API, build a document, sync a system
}, { args: { orderId: ctx.data.id } });
}
};Note the closure captures nothing from the surrounding scope — pass what it needs through opts.args and read it back on ctx.args.
No OAuth, because there is nothing to authorize across
Your code is already inside the store, so there is no token exchange, no scope grant screen, and no credential of the platform's to store. Data access comes from the sw.* bridges — sw.products, sw.orders, sw.customers, sw.records, plus sw.storage, sw.cache, sw.files, sw.csv, sw.jwt and others.
Third-party credentials — the ones you do still need — live in sw.secrets, encrypted at rest and write-only by default. You never read the value back; you reference it as {secret.KEY} and it is expanded at the HTTP or crypto boundary:
fetch("https://api.stripe.com/v1/charges", {
method: "POST",
headers: { Authorization: "Bearer {secret.STRIPE_SECRET_KEY}" }
});Your endpoints live on the merchant's own domain
Declare a script as "type": "route" with a method and route_path, export fetch, and you have a real HTTP endpoint served from the store's storefront origin — no separate deployment, no CORS dance for your own theme code.
module.exports.fetch = function (ctx) {
const req = ctx.request; // req.method, req.url, req.path, req.query, req.json()
return { status: 200, body: { success: true } };
};State-changing storefront requests need the CSRF token; a route that must accept calls from outside the browser — a provider webhook — declares "public": true and takes responsibility for verifying the caller's signature itself.
The development loop
The CLI is deliberately shaped like a version-control tool:
shopswired login
shopswired link # pick the store to develop against
shopswired init --type plugin --id my-plugin
shopswired dev --id my-plugin # store runs your local files, live
shopswired pull --type plugin --id my-plugin
shopswired push --type plugin --id my-plugindev watches your directory and the store executes your local files directly on each save — nothing is written to the store, so ending the session puts it back to its installed copy. push checks first whether anyone edited the plugin on the store since you last synced, and refuses rather than silently overwriting their work. That matters more than it sounds, because a merchant or a connected AI assistant can edit plugin files directly in the admin.
Distribution and getting paid
Publishing goes to the built-in Marketplace: merchants install from inside their admin, ShopsWired handles the billing, and the developer keeps 70% of each paid invoice via Stripe Connect. Tiered pricing is declared as ordered plan keys in the manifest ("plans": ["free", "pro", "biz"]) with the actual prices set at publish time and read at runtime as ctx.plan, so re-pricing is not a code change. Paid code ships with "source": "private" so installed shops can run it but not read it. New versions flow through the normal install-and-update path rather than a per-release approval queue. We covered that end to end in the marketplace guide.
The honest trade-offs
This model is not strictly better. Four things you should weigh:
- Market size. Shopify's merchant base is vastly larger than ShopsWired's. A ShopsWired plugin sells to ShopsWired merchants, full stop. If reach is the goal today, that is the honest counterargument, and no amount of developer ergonomics offsets it.
- No npm. The engine ships no packages and
require()resolves only inside your own plugin directory. The bridges cover a lot of what you would otherwise install — CSV, Excel, ZIP, JWT, SQL to your own external MySQL or Turso database — but if your integration leans on a large library, you port it, call it over HTTP, or push the work to a container job withsw.container. - The runtime is synchronous, with enforced budgets. There is no event loop to hide latency behind. You architect around the limits above instead of around them being generous.
- Less control. A hosted app is yours: your language, your dependencies, your database, your scaling. Here you are writing JavaScript inside someone else's runtime. That is exactly the constraint that removes the ops work, and exactly the reason some teams will not want it.
Which one to pick
If you are building a multi-platform integration that has to serve Shopify, BigCommerce and Magento merchants from one codebase, keep the hosted-app architecture — that is what it is for. If you are building a store's custom behavior, or a focused commerce extension you would rather ship than operate, the plugin model deletes most of the work that is not the feature.
Developing means having a shop to develop against. Pro is $99/mo — currently $1/mo for the first 3 months — with 0% platform transaction fees on every plan (you still pay your payment processor's standard rate). Full plan details are on pricing, and the complete Plugin API, entity shapes and worked recipes are in the docs.
← Back to Blog