portside-labs / laravel-shopify
A Laravel starter kit for building Shopify apps.
Package info
github.com/portside-labs/laravel-shopify
Type:project
pkg:composer/portside-labs/laravel-shopify
Requires
- php: ^8.3
- inertiajs/inertia-laravel: ^3.0
- laravel/framework: ^13.17
- laravel/tinker: ^3.0
- laravel/wayfinder: ^0.1.14
Requires (Dev)
- fakerphp/faker: ^1.24
- larastan/larastan: ^3.9
- laravel/boost: ^2.2
- laravel/pail: ^1.2.5
- laravel/pao: ^1.0.6
- laravel/pint: ^1.27
- laravel/sail: ^1.53
- mockery/mockery: ^1.6
- nunomaduro/collision: ^8.9.3
- pestphp/pest: ^5.1
- pestphp/pest-plugin-laravel: ^5.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
About Laravel Shopify
A starter kit for building embedded Shopify apps the Laravel way: Laravel, Inertia, and React, with pages built from Shopify's Polaris web components so they feel native in the admin.
What's included
- Authentication as a Laravel guard. App Bridge ID tokens are verified on every request.
$request->user()is the staff member,$request->shop()is their store, and routes are protected withauth. - Managed installation and token exchange. No OAuth redirects. Offline tokens (the shop) and online tokens (the staff member) are exchanged, stored, and refreshed automatically, and a revoked token makes the app reinstall itself.
- Webhooks as queued jobs. Deliveries are verified and dispatched by topic. The compliance topics,
app/uninstalled, andapp/scopes_updateare set up. - Polaris and App Bridge. The admin's sidebar, loading bar, and toasts are wired to Inertia.
- Translations. One set of files in
lang/, read with__()on the server andt()in React. - Billing through Shopify App Pricing. A
subscribedmiddleware and a Cashier-like API onShop. - Artisan commands to link the app, check its configuration, generate webhook jobs, and send test webhooks.
- Tests, static analysis, and formatting for all of the above.
Requirements
- PHP 8.3+, Composer, and Node.js 22+
- Shopify CLI
- A Dev Dashboard account and a development store
- For billing: a Partner Dashboard account, where Shopify App Pricing plans are set up
Getting started
composer create-project portside-labs/laravel-shopify my-app
cd my-app
-
Link a Shopify app and pull its credentials into
.env:php artisan shopify:install
The Shopify CLI lets you pick an app or create a new one. Locally, this links
shopify.app.dev.toml, which git ignores, and setsSHOPIFY_APP_CONFIG=devin.env, leavingshopify.app.tomlfor production. The kit's webhook subscriptions and settings are kept, and your app's client ID, name, URLs, and scopes are filled in. -
Start the app:
php artisan shopify:dev
This installs the Node packages and runs
shopify app dev, which opens a free Cloudflare tunnel (no account needed), starts the server, queue worker, and Vite, appliesshopify.app.dev.tomlto your development store, and installs the app there. Presspto open it.APP_URLin.env, andapplication_urlandredirect_urlsinshopify.app.dev.toml, follow the tunnel's URL. To use your own tunnel instead, pass it with--tunnel-url=https://my-app.ngrok.devand forward it tolocalhost:3000, or add another port to the URL.
Change your access scopes or webhooks in shopify.app.dev.toml; shopify:dev applies them as it runs. Billing needs its own setup before the subscribed middleware works; see Billing.
Artisan commands
| Command | What it does |
|---|---|
shopify:install |
Links the app and writes its credentials to .env. Locally, it links shopify.app.dev.toml. |
shopify:dev |
Serves the app through a tunnel and points APP_URL and the app's URLs in shopify.app.toml at it. Local environment only. Use --tunnel-url for your own tunnel. |
shopify:deploy |
Checks the configuration and releases it with shopify app deploy. --production deploys shopify.app.toml, --config=staging a named one, and --env=production reads .env.production. Passes on --message, --app-version, --no-release, --allow-updates, and --allow-deletes. |
shopify:doctor |
Checks credentials, URLs, API version, webhooks, and billing settings. Fails on errors, so it can run in CI. |
make:shopify-webhook orders/create |
Creates the job and a test, maps the topic in config/shopify.php, and subscribes to it in shopify.app.toml. |
shopify:webhook orders/create |
Sends a signed webhook to the app in-process, with no tunnel needed. Use --sync to run the job immediately and --payload=file.json to send your own payload. |
Using the kit
Admin API
$request->shop()->api()->graphql('{ shop { name } }')->json('data.shop');
Shop::api() acts as the shop. User::api() acts as the staff member, limited to their permissions. Both use Laravel's HTTP client, so Http::fake() works in tests.
Webhooks
Every webhook goes to /webhooks and is dispatched to the job mapped to its topic in config/shopify.php. Add one with php artisan make:shopify-webhook {topic}, then run php artisan shopify:deploy. Jobs receive the Shop and the payload.
Translations
Put every string in lang/ and read it with __() or t(), using Laravel's :placeholder and singular|plural syntax on both sides:
const { t } = useTranslation(); t('home.welcome', { app: name }); t('orders.count', { count: 3 }); // "{0} No orders|{1} One order|[2,*] :count orders"
The staff member's admin language is picked up from Shopify. To add a language, translate lang/{locale} and add the locale to app.locales in config/app.php.
Billing
Plans are sold through Shopify App Pricing, so billing works once the app is set up for it:
-
In the Partner Dashboard, switch the app's pricing to Shopify App Pricing and create its plans. Set each plan's welcome link to
/billing/welcome. -
Create a Partner API client with the "Manage apps" permission.
-
Add the app and the client to
.env. The handle is the part of the app's admin URL after/apps/, the app ID is shown in the Partner Dashboard, and the organization ID is the number in the dashboard's URL:SHOPIFY_APP_HANDLE=my-app SHOPIFY_APP_ID=1234 SHOPIFY_PARTNER_ORGANIZATION_ID=12345 SHOPIFY_PARTNER_ACCESS_TOKEN=prtapi_...
php artisan shopify:doctor notes any of these that are missing.
Gate routes with subscribed, or subscribed:pro for a specific plan:
Route::get('/reports', ReportController::class)->middleware('subscribed');
Shops without a plan are sent to Shopify's plan selection page. Check plans in code with:
$shop->subscribed(); // any active plan $shop->subscribed('pro'); // a specific plan $shop->onTrial(); $shop->subscription()?->plan;
Shopify sends no webhook when a subscription changes, so the plan is cached for five minutes. Development stores can take any plan for free.
Building pages
- Add pages to
resources/js/pages, built from Polaris web components. Link to routes with Wayfinder (resources/js/routes). - Register routes in
routes/web.phpbehindauth. - Authorize with policies using
$user->account_owner,$user->collaborator, and$user->scopes. - Show a toast from any controller with
Inertia::flash('toast', 'Saved.'). - Listen for
App\Events\ShopInstalledandShopUninstalledfor onboarding and cleanup. - Run
composer test(Pint, PHPStan, tests) andnpm run check(frontend lint and format).
Deploying
- Serve over HTTPS and run a queue worker.
- Set
APP_URL,SHOPIFY_API_KEY,SHOPIFY_API_SECRET, and the billing variables if you sell plans. - Link your production app to
shopify.app.tomlwithphp artisan shopify:install --env=production, which reads and writes.env.production. Point itsapplication_urlandredirect_urlsat your domain, and runphp artisan shopify:deploy --production --env=production. It refuses to deploy while the URL is still a tunnel or a placeholder. - Run
php artisan shopify:doctorto catch misconfiguration. It also warns whenSHOPIFY_API_VERSIONis nearing the end of support.
Working with AI agents
CLAUDE.md describes the kit's conventions for Claude Code. .mcp.json registers the Shopify Dev MCP server, and Laravel Boost adds Laravel tooling.
License
MIT. See LICENSE.
