devdojo/wave
Wave is a Laravel SaaS starter kit that gives you authentication, subscriptions and billing, user roles, an admin dashboard, blog, documentation, and customizable themes out of the box, so you can launch your product faster.
Wave is not a library you composer require — it's a full Laravel application you fork and own. Every day-to-day decision flows from that: you're not consuming an API, you're maintaining a codebase that upstream also ships.
Minimum path to a running app:
git clone https://github.com/thedevdojo/wave.git myapp
cd myapp
composer install
cp .env.example .env
php artisan key:generate
# edit .env: DB_*, APP_URL, and any billing keys
php artisan migrate --seed
php artisan storage:link
npm install && npm run dev # separate terminal: php artisan serve
Some releases ship an installer (php artisan wave:install) that prompts for DB credentials and seeds for you — try it first; fall back to the manual steps above.
Where to look first, in this order:
.env.example — it's effectively the feature matrix. If a key is there (STRIPE_*, PADDLE_*, mail, demo flags), that feature is wired up.config/wave.php (or config/wave/*) — toggles for blog, docs, changelog, API, billing, themes. This tells you what's "on" before you read a single controller.routes/web.php and routes/api.php — the shape of the whole app in ~200 lines.app/Models/User.php — roles, subscriptions, and traits are attached here; it's the hub.database/migrations + database/seeders — roles, plans, settings, and the demo content all live here.First use case to validate end-to-end: log in as the seeded admin, create a plan, register a second user in an incognito window, and run a Stripe test-mode checkout with stripe listen --forward-to localhost:8000/stripe/webhook. That one thread exercises routing, middleware, roles, billing config, webhooks, and the dashboard layout — the fastest way to understand how the pieces fit.
Treat config as feature flags. Before writing features, turn off the sections you don't need (blog, docs, changelog, API) in config/wave.php/env. Fewer routes and views in the tree means less noise when you grep and less to merge on upgrade.
Gate premium features in one place. Wave gives you a role_id on users plus a subscription/plan concept. Don't scatter if (auth()->user()->subscribed()) through controllers. Add one middleware (EnsureSubscribed / EnsurePlan) and group premium routes:
Route::middleware(['auth', 'subscribed'])->group(function () {
Route::get('/app/reports', ReportController::class);
});
Then feature checks stay consistent when you add trials, grace periods, or per-plan limits later.
Extend the dashboard, don't replace it. New pages should live inside the existing authenticated layout and follow the established component conventions (Blade components, Livewire components if the version ships them). You get nav, styling, flash messages, and impersonation for free — and your diff stays small.
Add roles via data, not code. Roles are rows, not enums. Insert them in a seeder/migration and gate with @can/middleware rather than hardcoding role IDs. Hardcoded role_id === 1 is the most common thing that breaks when someone re-seeds.
Webhooks: idempotent and queued. Billing webhook handlers run outside the normal request lifecycle. Make handlers idempotent (key on the Stripe/Paddle event ID), push side effects onto a queue, and always verify signatures. Keep the webhook route out of the CSRF-protected web group.
Keep an upstream remote. This is the single highest-leverage habit:
git remote add wave https://github.com/thedevdojo/wave.git
git fetch wave
git checkout -b upgrade/wave-YYYY-MM
git merge wave/main # resolve, test
Do upgrades on a branch, tag your releases, and keep custom code in your own namespaces (App\..., resources/views/app/...) so merges stay tractable.
API surface stays separate. If the API section is enabled, it's Sanctum-token based and versioned under /api. Keep API controllers and resources distinct from web controllers so you can evolve one without breaking the other.
Deploy like a normal Laravel app. npm run build → php artisan migrate --force → config:cache → route:cache → view:cache, with a real queue worker and storage:link (or S3). Wave's surface area is large; route/view caching noticeably helps.
composer update will not upgrade Wave. You own the code. Budget time for upstream merges, and keep your diffs small and localized — the number one cause of "we can never upgrade" is editing core files in place.[email protected] / password). Change it, delete it, or gate the admin area before anything goes public. Check database/seeders for exactly what your version creates.WAVE_DEMO=true-style env flags will surprise you in staging — verify it's false in production.plans row pointing at a Stripe price that doesn't exist (wrong mode: test vs live, or a deleted price) fails at checkout with a confusing redirect rather than an exception. Always test checkout in both modes after touching plans.STRIPE_WEBHOOK_SECRET (or the Paddle equivalent) is unset or wrong, payment succeeds but the subscription never activates. Verify with stripe listen locally and check the webhook endpoint's response codes in the provider dashboard in production.QUEUE_CONNECTION=sync will bite you. Emails, webhook side effects, and notifications block the request. Set a real driver and run a worker before you invite testers.php artisan config:clear cache:clear. "My change did nothing" is usually the cache.migrate:fresh --seed wipes real data, and demo seeders will happily recreate demo users in production. Scope seeders and guard them by environment.username rather than id; if a route 404s unexpectedly, check getRouteKeyName() rather than assuming a routing bug.php artisan route:list --path=checkout, tail -f storage/logs/laravel.log, the Stripe CLI for webhooks, and Telescope if it's installed. php artisan test right after install gives you a baseline — the shipped feature tests are also good templates for your own.How can I help you explore Laravel packages today?