---
title: "Installation — Purchases for Laravel | Roundly"
description: "Install via Composer, publish the publish-only migrations, and optionally publish the config and the webhook routes file."
url: https://roundly-consulting.com/open-source/docs/purchases-for-laravel/installation
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Purchases for Laravel](https://roundly-consulting.com/open-source/docs/purchases-for-laravel.md)

# Installation

Require the package, then publish and run the migrations. The service provider is auto-discovered, and Composer pulls in the Roundly crypto, enums, money and package-toolkit packages it builds on:

```bash
composer require roundly-consulting/purchases-for-laravel

php artisan vendor:publish --tag="purchases-migrations"
php artisan migrate
```

The migrations are publish-only — the package never auto-loads them, so a bare php artisan migrate creates nothing until you publish the purchases-migrations tag. Republishing overwrites the files in place instead of adding a second copy. The migrations are forward-only — they have no down(), so migrate:rollback skips them; drop the tables yourself if you ever need to remove them.

## Config and routes

The config file is optional — every key has an env-backed default. Publish it to override, and publish the routes file only if you want to wire the bundled webhook route yourself:

```bash
php artisan vendor:publish --tag="purchases-config"
php artisan vendor:publish --tag="purchases-routes"
```

## Guided installer

purchases:install publishes the config and migrations and offers to run the migrations. With --providers it asks which providers you use and appends their env keys to .env — only those it doesn’t define yet, because a later blank duplicate would win, so re-running it never wipes a secret you already filled in. Google’s block includes the push-authentication keys:

```bash
php artisan purchases:install
php artisan purchases:install --providers   # choose providers, append their missing .env keys
```

Fill in each provider’s credentials — Stripe needs both PURCHASES\_STRIPE\_SECRET and PURCHASES\_STRIPE\_WEBHOOK\_SECRET — then check them with purchases:providers and purchases:verify (see Artisan commands).

## UUID or ULID owners

Purchases and subscriptions belong to an owner through a nullable polymorphic owner column. If your owner models use UUID or ULID primary keys, set the key type before running the migrations — all owner models must share one key type. A value other than bigint, uuid or ulid throws an InvalidConfigurationException naming the key, so a typo fails the migration instead of keying the owner column wrong:

```bash
# Set before running the migrations when owners use UUID or ULID keys
PURCHASES_KEY_TYPE=uuid
```

The package exposes three publish tags: purchases-config, purchases-migrations and purchases-routes.

## Upgrading from 1.0

The 1.1 releases change a few behaviours a host may rely on:

- Catch VerificationException, not RequestException, for a client’s bad id, token or receipt from callback() — Stripe, Google and Apple alike. The store’s RequestException is its getPrevious(); see Client callbacks.
- SubscriptionStarted — not SubscriptionRenewed — fires when a subscription created before it was paid for (Stripe incomplete, Google pending) first becomes active, so owner-linking listeners run for 3D Secure and default\_incomplete subscriptions too.
- A one-off Stripe invoice recorded from invoice.paid carries the PaymentIntent that paid it as its transaction\_id, so its refunds and disputes link to it and flip it to Refunded. Set PURCHASES\_STRIPE\_SECRET.
- A Google RTDN for another app (a shared Pub/Sub topic) is audited and answered 204, never applied.
- Stripe and Google refunds record a refunded\_at, and provider dates keep their real instant on a host whose app.timezone isn’t UTC.
- purchases:providers reports Stripe as ready only with both secrets, and Google only once its pushes can be authenticated.
- Tests: assertSubscriptionStarted() passes only when SubscriptionStarted really fired — use assertSubscriptionRecorded() for “a subscription result arrived” — and FakeResult uses one id as both provider and transaction id.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/purchases-for-laravel.md)
