← Back to Blog
Developers

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:

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_deleteproduct.*, 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-plugin

dev 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:

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