Developers
Extending MoneyMate
Architecture, local setup, how to find and edit any existing page's JSX, environment variables, migration conventions, and copy-paste recipes for a new CRUD feature, page-builder block, plan-gated feature, notification, or admin page.
Everything on this page is provided so you can extend and customize the application — but per CodeCanyon's item support policy, support covers using the item as delivered and fixing genuine bugs in the original code. It does not cover code you've modified, extended, or combined with third-party additions, or issues that only appear after such changes. Once you edit application code, you're on your own for anything related to that change — see Getting support for the full policy.
- The stack, in practice
- Architecture principles
- Local development setup
- Environment variables
- Installable as a PWA
- Compiling frontend assets
- Editing an existing page's UI
- Where things live
- Database & migration conventions
- Recipe: a brand-new CRUD feature end-to-end
- Recipe: a new page-builder block type
- Recipe: a new plan-gated feature
- Recipe: a new notification preference
- Recipe: a new admin settings page
- Frontend conventions
- The built-in REST API
- Testing conventions
- Deployment checklist
The stack, in practice
MoneyMate is a standard Laravel 13 application using Inertia.js v3 with React 18 — the web app itself renders through Inertia, not a client-side call to a REST layer. Every "page" is a Laravel controller returning Inertia::render('SomeFolder/SomeComponent', [...props]), matched to a React component under resources/js/Pages/SomeFolder/SomeComponent.jsx. Routing, auth, and validation all stay in Laravel exactly as you already know them; React only owns rendering and client-side interaction. Separately, the product also ships a full token-authenticated REST API for third-party integrations — see that section for the complete endpoint list.
If you've built a normal Laravel + Blade app before, the mental model is: replace every return view(...) with return Inertia::render(...), and replace the Blade template with a React component that receives the same data as props instead of view variables. There's no client-side router to configure either — Inertia's <Link> component makes a real request to a real Laravel route, and the server decides what page component comes back, exactly like a traditional server-rendered app, just without the full-page reload.
Since Laravel 11, new projects no longer have an app/Http/Kernel.php or app/Console/Kernel.php — that structure was replaced by a single bootstrap/app.php, and this Laravel 13 project follows that same current convention. Middleware registration, route file loading, and exception handling all live in that one file. If you've only worked with Laravel 10 or earlier, that's where to look first instead of a Kernel class.
Architecture principles
A few conventions repeat throughout the codebase — knowing them up front makes everything else in this guide easier to follow:
Thin controllers, real services
Controllers validate input, call a service or repository, and return an Inertia response. Actual business logic (report queries, plan-limit checks, the debt payoff simulation, package/Stripe sync) lives in app/Services, not inline in a controller method.
One enforcement point per rule
PackageLimitService is the only place that ever answers "can this user do X" — controllers call it rather than re-implementing a check. Copy this pattern for your own gated features instead of scattering if ($user->package->...) checks around.
The real path, not a parallel one
AI-assisted actions (auto-categorize, natural-language entry) and bulk import both create records through the exact same ExpenseService/IncomeService methods a manual form submission uses — never a separate insert path that could drift out of sync with validation or side effects (budget recalculation, rule matching, etc).
Two-track database changes
Every schema/content/translation change ships as both a seeder update (for fresh installs) and a small, non-destructive migration (for installs already running in the wild). See Database & migration conventions below.
Local development setup
composer install npm install cp .env.example .env php artisan key:generate php artisan migrate --seed npm run dev
npm run dev starts Vite's dev server with hot module replacement — leave it running while you work. For a one-off production build instead, use npm run build (see below).
- Run the test suite:
php artisan test(or./vendor/bin/pestdirectly — the project uses Pest) - Code style:
./vendor/bin/pintformats PHP to the project's Laravel-style ruleset; run./vendor/bin/pint --dirtyto only touch files you've actually changed - Seed demo data any time:
php artisan db:seed --class=DemoDataSeeder— safe to re-run, populates the 10 demo subscriber accounts described in Installation. It creates no admin account, and its accounts get a random unrecorded password unless you setDEMO_SEED_PASSWORDin.envfirst
Environment variables
Most configuration is meant to be set through the admin panel after installation (see System Settings and AI Providers) rather than by hand-editing .env — the installer writes the core ones for you. The values you're most likely to touch directly during development:
| Variable | Purpose |
|---|---|
APP_ENV | local for development, production on a live server — affects error detail shown, config caching behavior, and a few installer safety checks. |
APP_DEBUG | Set to false in production — a debug page that leaks stack traces and env values is a real security risk on a live financial app. |
APP_URL | Must match your real domain exactly (including https://) — used to generate absolute URLs in emails, Stripe redirects, and signed data-export links. |
DB_* | Database connection — written by the installer's Database step, or set manually if you're skipping the installer entirely. |
QUEUE_CONNECTION | database by default (needs a queue worker — see cPanel Hosting or VPS Hosting), or sync to run queued jobs inline with no worker at all. |
SESSION_DRIVER | database by default; fine to leave as-is unless you have a specific reason to change it (e.g. Redis at scale). |
Stripe keys, SMTP credentials, and AI provider API keys are not read from .env at runtime — they're stored encrypted in the database via the admin panel (ProviderSetting model) so they can be changed without redeploying or restarting anything. If you're grepping the codebase looking for where STRIPE_SECRET or similar is used, that's why you won't find it wired to config() the usual Laravel way.
Installable as a PWA
The app ships a web app manifest, served dynamically at runtime rather than as a static build-time file — so a browser's "Install app" prompt picks up whatever name and icon are currently set in Site Settings (application_name, favicon), with no rebuild needed after changing either. Visiting the app in a supporting browser (Chrome, Edge, and most mobile browsers) offers to install it as a standalone app — no separate app-store build, wrapper, or Capacitor/Cordova-style shell involved.
Compiling frontend assets for production
Whenever you change anything under resources/js or resources/css, rebuild the compiled assets before deploying:
npm install npm run build
This runs Vite's production build and writes hashed, minified bundles into public/build, along with a manifest.json Laravel's @vite Blade directive uses to resolve the correct file. Commit or upload the contents of public/build along with your code — the production server does not need Node.js installed at all if you deploy pre-built assets (this is exactly what the cPanel hosting path assumes, since most shared hosts don't offer Node).
Laravel serves the React/JS bundle Vite already built — editing a .jsx file directly on the server does nothing until you run npm run build again (or run npm run dev locally while developing).
Editing an existing page's UI
This is the single most common thing a buyer customizing this application wants to do — change a label, tweak a layout, add a field to an existing form, restyle a card — on a screen that already exists, rather than building something new. The process is always the same four steps:
Note the URL
Open the page you want to change in the browser and look at the address bar — say
/expense.Find the route
Search
routes/web.phpfor that URI (grep -n "'expense'" routes/web.php) to find which controller and method handle it — for exampleExpenseController::index.Find the Inertia component name
Open that controller method and look for its
Inertia::render('SomeFolder/SomeComponent', [...])call — that string is a direct path.Edit the matching JSX file
Inertia::render('Expenses/Index', ...)means the file isresources/js/Pages/Expenses/Index.jsx— open it, make your change, save.
Worked example — changing the "Add Expense" button's label and adding a new read-only field to that form:
grep -n "expense" routes/web.php
# ...
# Route::get('/', [ExpenseController::class, 'index'])->name('expense.index');
# app/Http/Controllers/ExpenseController.php
# return Inertia::render('Expenses/Index', [...]);
# → the file to edit is:
resources/js/Pages/Expenses/Index.jsx
Inside that file, the button is a plain JSX element like any other React code:
<button type="button" className="btn btn-primary" onClick={() => modalRef.current.show()}>
<IconCalendarDollar size={18} /> {t('add_expense')}
</button>
Change the icon, the classes, or wrap it differently — it's ordinary JSX/Tabler markup, nothing framework-specific to learn beyond what's already on the page. If the text comes from t('add_expense') rather than being hardcoded, change the actual wording in Translations instead of the JSX (see the callout below) — editing the JSX only changes which translation key is looked up, not the words shown.
To add a new field to the same form, find the <form> further down the same file (search for id="addExpense" or the modal's <Drawer>/modal wrapper), copy an existing field block (label + input + value={data.x} + onChange={(e) => setData('x', e.target.value)}), and rename it. Remember: a genuinely new field also needs a matching column (migration), a spot in the controller's validate()/FormRequest rules, and to be included wherever the record is created/updated — the JSX half alone only gets you a field that doesn't actually save.
Quick reference — "I want to change X"
| You want to change... | Edit this |
|---|---|
| Marketing site wording/images (Home, Features, Pricing, etc.) | Nothing in code — use the Page Builder. This content is database-driven by design. |
| User-panel sidebar links, order, icons | resources/js/Layouts/AppLayout.jsx — the NAV_GROUPS array |
| Admin sidebar links, order, icons | resources/js/Layouts/AdminLayout.jsx — the NAV_GROUPS array |
| The topbar (theme switcher, notification bell, account dropdown) | Same two layout files — the <header className="admin-topbar"> block near the bottom of each |
| A specific page's layout, text, or fields | That page's own file under resources/js/Pages/... — see the four-step process above |
| A dashboard widget's label or number | resources/js/Pages/Dashboard/Index.jsx (user panel) or resources/js/Pages/Admin/Analytics/Index.jsx (admin) — each widget is a <MetricCard>/<ChartCard> call with its own props |
| A modal/create form shared across a page (add fields, reorder, change validation messaging) | The same page file — the modal markup lives directly below the list/table in the same component, not a separate file |
| Colors, spacing, or component look across the whole admin/app panel | resources/css/admin-tokens.css (CSS custom properties — colors, radii, shadows) and resources/css/admin.css (component styles built from those properties) |
| Colors or layout across the whole public marketing site | resources/css/public.css (its own separate set of CSS custom properties, deliberately not shared with the admin/app panel) |
| A specific page-builder block's markup (Banner, Feature Grid, etc.) | resources/js/Components/Blocks/BlockName.jsx — see adding a block type for the full pattern this follows |
Any UI string ("Add Expense", a validation message shown via t(...)) | Translations in the admin panel, not the JSX — only add/rename a translation key in code, never hardcode new wording directly into a component |
Before editing, check whether what you're looking at is actually defined on the page you're viewing, or inherited from something shared — AppLayout.jsx/AdminLayout.jsx (sidebar, topbar), a component under resources/js/Components (used by many pages), or resources/css/admin.css/public.css (global styles). Editing a shared file changes every page that uses it — exactly what you want for something like the sidebar, but a common mistake if you only meant to change one page and accidentally edited a component ten other pages also render.
The edit → verify loop
Run the dev server
npm run devand keep it running. Every save to a.jsx/.cssfile hot-reloads in the browser within a second or two — no manual refresh, no rebuild step, while you're actively working locally.Check both themes
Toggle light/dark mode (the theme switcher in the topbar) after any styling change — a color that reads fine in light mode is a surprisingly common way to end up invisible in dark mode, since both panels and the public site fully support both.
Check the actual data states, not just the happy path
An empty list, a very long value, and a value near a plan limit are the three states most likely to reveal a layout you only tested against tidy demo data.
Build before deploying
npm run build, then upload the regeneratedpublic/buildfolder — see Compiling frontend assets above. A change that looks correct undernpm run devlocally still does nothing on the live server until this step happens.
Where things live
| Path | What's there |
|---|---|
app/Http/Controllers | One controller per feature area; admin-only controllers live under Admin/ |
app/Models | Eloquent models |
app/Services | Business logic that doesn't belong in a controller — PackageLimitService, report repositories, the debt payoff planner calculator, etc. |
app/Notifications | Every email/in-app notification class |
app/Console/Commands | Scheduled commands (see routes/console.php for when each one runs) |
resources/js/Pages | One React component per Inertia page, mirroring the controller structure |
resources/js/Layouts | AppLayout.jsx (user panel shell), AdminLayout.jsx (admin shell), PublicLayout.jsx (marketing site shell) |
resources/js/Components | Shared UI components, including Components/Blocks (page-builder block renderers) and Components/Admin (shared admin widgets like MetricCard/ChartCard) |
app/Support/BlockRegistry.php | Server-side source of truth for page-builder block types and their default content |
resources/js/blockSchemas.js | Client-side mirror of the same registry, used by the page-builder editor UI |
database/seeders/PackageSeeder.php | The three default pricing tiers and every feature/limit flag |
database/seeders/TranslationSeeder.php | Every UI string for a fresh install, in English |
routes/web.php / routes/console.php | HTTP routes and the scheduler, respectively |
app/Http/Requests | Form request validation classes, including the admin-only ones under Admin/ |
app/Repositories | Query-building classes for data that's read in more than one shape — the expense/income report repositories are the main example |
database/migrations | Schema changes, in the order they run — see the naming convention below |
database/factories | Model factories used by the test suite |
tests/Feature / tests/Unit | Pest test files — see Testing conventions |
Database & migration conventions
This project maintains two installation paths that must never drift apart: a brand-new install (which runs every migration from scratch, then a seeder) and an existing install being upgraded (which only runs whatever new migrations have shipped since the last update). Every schema, content, or translation change is written to work correctly on both:
The migration is the source of truth for existing installs
A real schema change (a new column, table, or index) is always a migration under
database/migrations, named with a date prefix like2026_07_28_000001_add_weekly_digest_package_gating.php. Content-only changes (new translation strings, a backfilled default value on existing rows) are also shipped as a migration usingDB::table(...)->updateOrInsert(...)— non-destructive by design, so it never overwrites an admin's own edited content or a translator's non-English strings.The seeder is the source of truth for fresh installs
Whatever the migration changed for existing installs must also be reflected in the relevant seeder (
PackageSeeder,TranslationSeeder,BuilderPageSeeder,MenuSeeder, etc.) so a brand-new install ends up in the exact same state without needing every historical migration's content logic to "replay" correctly. Concretely: if you add a translation key via a migration, add the identical key/value pair toTranslationSeeder.phptoo — it truncates and re-seeds the whole translations table on a fresh install, so a key that only exists in the migration would be silently missing.Never edit an already-shipped migration
Once a migration has been included in a release, treat it as immutable — anyone who already installed or updated will have already run it. A follow-up change is always a new migration, even to fix a mistake in an earlier one.
Recipe: a brand-new CRUD feature end-to-end
Most of the personal-finance features in this application (Bank Accounts, Liability Accounts, Savings Goals, and so on) are the same shape: a user-owned model, a repository, a thin service, a controller, a form request, and a single Inertia page handling list/create/edit/delete without separate routes for each. Here's that shape, start to finish, using a hypothetical Vehicle feature as the example:
Migration & model
Create the table (always include
user_idand scope every query to it) and an Eloquent model with$fillableand any casts. Mirror an existing simple model likeApp\Models\AssetAccountfor the shape.Repository
A class with
getByUserId(),create(),update(),delete()methods — this is where you'd compute a derived value (like converting to a reporting currency) so the controller stays thin.FormRequest
Validation rules for store/update, scoped so a user can only reference their own related records (e.g.
Rule::exists('currencies', 'id'), orRule::exists('bank_accounts', 'id')->where('user_id', $userId)for anything owned).Controller
index()returns the Inertia page with the user's existing records;store()/update()/destroy()are simple redirects back toindexwith a flash message (via thenotyf()helper already used throughout the app). Wrap any multi-step write inDB::transaction().Routes
Add a
Route::group(['prefix' => 'vehicles'], ...)block toroutes/web.phpinside the authenticated group, namedvehicles.index/vehicles.store/etc., matching the naming pattern every other feature in the file already uses.Inertia page
One React component at
resources/js/Pages/Vehicles/Index.jsx: a table of existing records, and a modal (see any existing simple CRUD page, likeSavingsGoals/Index.jsx, for the exact modal +useFormpattern) for create/edit.Navigation entry
Add
{ key: 'vehicles', route: 'vehicles.index', label: 'vehicles', icon: IconCar }to the relevant group inNAV_GROUPSinsideresources/js/Layouts/AppLayout.jsx, and the matching translation key/value inTranslationSeeder.php(plus a migration if the app is already installed anywhere — see above).Gate it behind a plan, if it should be (optional)
Follow the plan-gating recipe below.
Tests
A feature test covering ownership scoping (user A can never see/edit user B's records) at minimum — this is the single most common real bug class in a multi-tenant CRUD feature.
Recipe: adding a new page-builder block type
Every marketing-page content block (Banner, Feature Grid, Pricing Table, etc. — see Page Builder & Menus) follows the same four-file pattern. To add a new one, say a video_embed block:
Register the type server-side
Add
'video_embed'to the array inApp\Support\BlockRegistry::types(), and a default content shape indefaultContent()— this is the only server-side validation of whattypevalues are allowed.Mirror it client-side
Add a matching entry to
BLOCK_SCHEMASandDEFAULT_CONTENTinresources/js/blockSchemas.js— this drives the admin editor's field list (label, kind: text/textarea/richtext/select/image/repeatable) and instant local preview.Build the renderer
Create
resources/js/Components/Blocks/VideoEmbed.jsx— a plain React component receiving{ content }and rendering the public-facing HTML for that block.Wire it up
Add
video_embed: VideoEmbedtoBLOCK_RENDERERSinresources/js/Components/Blocks/index.js.
That's it — the block immediately appears in the admin builder's block palette and renders on any page it's added to. No database migration needed; block content is a JSON column.
Recipe: adding a new plan-gated feature
Every feature you can turn on/off per pricing tier follows the same pattern (see Packages):
Add the limit key
Add a new key under
features.limitsfor each tier indatabase/seeders/PackageSeeder.php(and add it to the existing package rows via a migration if the app has already been installed anywhere).Add it to the admin form
Add an entry to the appropriate group in
FEATURE_GROUPSinsideresources/js/Components/Admin/PackageForm.jsx, and to the validation whitelist inStorePackageRequest/UpdatePackageRequest.Gate the real feature
In the controller/service that powers the feature, call
PackageLimitService::isFeatureEnabled($user, 'your_new_key')(boolean flags) or::canCreate($user, 'your_new_key', $currentCount)(numeric limits, wheretruestored in the database means unlimited).Advertise it (optional)
Add it to
PackagePublicDataService::COMPARISON_ROWSso it shows up automatically in the public Pricing page's comparison table.
Recipe: adding a new notification preference
User-controllable notification toggles (see Notification Preferences) live on the NotificationPreference model. Add a new boolean column via a migration, add it to NotificationPreferenceController's only()/validate() lists, and read it from your Notification class's via() method — the established pattern is return $preference->your_flag ? ['database', 'mail'] : ['database']; so the in-app bell always fires but email is opt-in.
Recipe: adding a new admin settings page
Every screen under the admin sidebar (see Admin Panel) follows the same shape as a normal CRUD feature, with two differences: the controller lives under app/Http/Controllers/Admin, and the route is registered inside the admin middleware group.
Controller & routes
Add your controller under
Admin/, and register its routes inside the existingRoute::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')group inroutes/web.php— theadminmiddleware alias (EnsureUserIsAdmin) is what actually blocks non-admin users with a 403.Inertia page
Create the page under
resources/js/Pages/Admin/YourFeature/Index.jsx, wrapped in<AdminLayout>rather than<AppLayout>— this is what gives you the admin sidebar/topbar shell instead of the user-panel one.Sidebar entry
Add an entry to the appropriate group (or a new group) in
NAV_GROUPSinsideresources/js/Layouts/AdminLayout.jsx.Reuse the shared admin components
PageHeader,EmptyState,StatusBadge,RowActionsMenu,Drawer,MetricCard, andChartCard(all underresources/js/Components/Admin) cover the vast majority of admin UI patterns already — a table with a "..." row-actions menu, a slide-out create/edit drawer, an empty state, a dashboard-style metric card. Reach for these before building a one-off equivalent.
Frontend conventions
- UI library: the admin and user panels use the Tabler class-based design system (plain Bootstrap-flavored classes like
card,btn btn-primary,table table-vcenter) — there's no component library abstraction layer to learn, just apply the same classes Tabler's own docs describe. - The public marketing site is a separate design system — its own CSS custom properties and
public-*-prefixed classes inresources/css/public.css, deliberately not mixed with Tabler. - Translations: every visible string in the user/admin panels goes through
t('some_key')fromuseSharedProps(), resolving against thetranslationsInertia shared prop for the current locale (falling back to the raw key if missing — a quick way to spot an untranslated string during development). Add new keys toTranslationSeeder.phpand a migration, per the conventions above. - Icons: @tabler/icons-react throughout — the page-builder's feature-grid blocks even resolve an icon by string name at runtime (
TablerIcons[item.icon]), so admin content can reference any icon in the set without a code change. - Forms: Inertia's own
useForm()hook is used for every create/edit form — it tracks field state, in-flight submission, and validation errors from the last response together, without a separate form library.
The built-in REST API
MoneyMate doesn't just expose a couple of endpoints — it ships a full, already-built REST API under routes/api.php, versioned at /api/v1, covering essentially every core money-management resource in the app. Every route requires a subscriber's personal access token — generated from API Tokens, issued via Laravel Sanctum — sent as Authorization: Bearer <token>, and both the auth:sanctum and a custom EnsureApiAccessEnabled middleware run on every request, so a token only works for plans with the api_access feature enabled. The full request/response shape for each endpoint is documented in-app, right on the API Tokens page.
Resources covered, all scoped to the token's owner by an explicit where('user_id', ...) lookup in each controller — not plain route-model binding, so no route can resolve another subscriber's record by id:
GET /user— the authenticated subscriber's profile- Full CRUD on
incomesandexpenses - Full CRUD (except create/edit views) on
budgets,tags,recurring-transactions, and transactionrules - Full CRUD on
savings-goals, plus aPOST /savings-goals/{id}/contributeaction debts— list, create, show, delete, plusrepayandcollectactionsGET/POSTonaccounts,categories, andbanksGET/POSTon accounttransfers- Read-only
currenciesandnet-worth
That's 15 resources and 50+ endpoints already built, tested, and gated behind the same plan-limit and validation logic as the web app — ready to hand to buyers building a mobile app, a Zapier-style automation, or a public API product on day one, with no extra backend work required.
To expose more endpoints of your own, add routes to routes/api.php inside the existing auth:sanctum + EnsureApiAccessEnabled group, and reuse the same services/repositories the Inertia controllers already call. The business logic layer described in Architecture principles is what makes this safe to do without duplicating validation or plan-limit checks.
Testing conventions
The project uses Pest. Feature tests live in tests/Feature (grouped into subfolders like Admin/ and Public/ where relevant) and unit tests in tests/Unit. Run the whole suite with:
php artisan test
A typical feature test authenticates as a user, performs the request, and asserts on both the HTTP response and the resulting database state:
test('a user can create a savings goal', function () {
$user = User::factory()->create();
$this->actingAs($user)->post(route('savings-goals.store'), [
'name' => 'Emergency fund',
'target_amount' => 5000,
])->assertRedirect();
expect(SavingsGoal::where('user_id', $user->id)->where('name', 'Emergency fund')->exists())
->toBeTrue();
});
New features should ship with tests that cover the actual correctness claim being made (a limit really blocks at the boundary, a notification really respects the preference, a calculation really matches the formula, one user can never see another user's records) — not just that a page returns a 200.
Licensing (CodeCanyon purchase code verification)
Seller only The web installer can require a valid Envato purchase code before anyone can proceed past the first step. This is entirely optional and off by default — it only activates once two values are set in .env: ENVATO_PERSONAL_TOKEN and ENVATO_ITEM_ID. Leave them empty for local development; the license step is skipped completely when they're unset, so this never gets in your way while building or testing.
Generate a scoped token
At build.envato.com/create-token, create a personal token with only the "Verify Purchase Codes" permission. Don't grant any broader scope — this token ends up inside the ZIP you upload to CodeCanyon, so it should only be able to do the one thing this feature needs.
Find your item ID
Once the item is listed, its numeric ID is in the item's CodeCanyon URL and in your Envato author dashboard.
Set both values before packaging
Add
ENVATO_PERSONAL_TOKENandENVATO_ITEM_IDto the.envthat ships inside your distributable ZIP — not the.env.examplecommitted to your own repo, and not your local development.env. With both set, a buyer running the installer sees a "Verify Your Purchase" step first; entering a purchase code that doesn't match this item's ID, or doesn't verify at all, is rejected.
Verification happens once, during installation — the result (buyer username, license type, verification timestamp, and a hash of the purchase code) is stored in site settings, not re-checked on every request. This keeps the app usable offline after install and avoids "phoning home" on a schedule.
The purchase code itself is never stored — only a SHA-256 hash of it, kept for support reference.
Deployment checklist
After pulling code changes onto a live server:
Install dependencies
composer install --no-dev --optimize-autoloaderand, if you changed any frontend code,npm install && npm run buildon a machine with Node — the server itself never needs Node or Composer if you upload the already-builtvendor/andpublic/buildfolders as-is (see cPanel Hosting or VPS Hosting).Run new migrations
php artisan migrate --force— nevermigrate:freshagainst a live database with real subscriber data.Rebuild caches
php artisan config:cache,php artisan route:cache,php artisan view:cache.Restart the queue worker, if applicable
A running
queue:workprocess caches the code it started with — restart it (or let the next cron-triggered--stop-when-emptyrun pick up the change automatically) so queued jobs use your updated code.