A strictly-typed, production-ready Laravel 13 API starter.
Domain-driven structure · Actions + Repositories · Auth, RBAC, Stripe, OAuth & OTP — batteries included.
A clean foundation for building serious JSON APIs with Laravel. It is API-only (no Blade, no sessions), organized into self-contained domains, and wired so that controllers stay thin: validation lives in form requests, business logic in single-purpose actions, and every database touch goes through a repository interface. Static analysis runs at Larastan level max with zero errors, and the whole thing is covered by 110 passing tests.
Authentication & Access
- Sanctum token authentication (register, login, logout, device tokens)
- Spatie RBAC — roles & granular permissions,
admin/userout of the box - OAuth 2.0 social login (Google & Apple) via Socialite
- Email verification with signed URLs + email OTP flow for verifying/changing an email
- Password reset with a frontend-friendly reset URL
Product features
- Admin API — user CRUD with search, role filter, sort & pagination; role & permission management
- Stripe subscriptions via Cashier — plans, Checkout, billing portal, cancel/resume, webhooks
- Audit trail — every authenticated request is logged via Activitylog
- Health check (database + cache) for uptime monitoring
- Auto-generated OpenAPI docs via Scramble at
/docs/api
Engineering
- PHP 8.4 with
declare(strict_types=1)everywhere,finalclasses, constructor promotion - Actions + Repositories + DTOs with a uniform
ApiResponseJSON envelope - PHPStan / Larastan at level max, Pint for style, Rector for automated refactors
- Pest test suite · Octane-ready · Docker Compose (Nginx + PHP + Postgres)
- GitHub Actions CI (lint + test matrix)
The app is organized by domain folders under app/ instead of Laravel's default
Http/Models/... layout. Each domain owns its controllers, requests, DTOs, actions, resources,
models, and repositories. A request flows in one direction:
HTTP request
→ routes/api/*.php versioned under /api/v1, grouped by access level
→ {Domain}\Requests\*Request validation + authorization
→ {Domain}\Controllers\* thin — no business logic
→ {Domain}\Data\*Data (DTO) immutable, built from validated input
→ {Domain}\Actions\*Action one public execute(); the business logic
→ {Domain}\Repositories\* interface + Eloquent impl; all DB access
→ {Domain}\Resources\*Resource response shaping
→ Support\Http\ApiResponse uniform JSON envelope: { data, message, meta? }
Repository interfaces are bound to their Eloquent implementations in RepositoryServiceProvider, so
domains depend on contracts, not Eloquent. Untyped input (validated arrays, query criteria) is read
through the typed Support\Data\Input helper rather than raw casts — which is part of how the
codebase stays clean at PHPStan level max.
Full guide: ARCHITECTURE.md · Quick map for tools & agents: docs/PROJECT_MAP.md
| Area | Choice |
|---|---|
| Language / Framework | PHP 8.4 · Laravel 13 |
| Auth | Sanctum · Socialite (Google, Apple) |
| Authorization | Spatie laravel-permission |
| Billing | Laravel Cashier (Stripe) |
| Audit | Spatie laravel-activitylog |
| API docs | Scramble (OpenAPI) |
| Runtime | Laravel Octane-ready |
| Testing | Pest · PHPUnit |
| Quality | Larastan (level max) · Pint · Rector |
| Database | SQLite (dev) · PostgreSQL (prod) |
git clone <repo-url> && cd laravel_api_only_boilerplate
composer install
cp .env.example .env
php artisan key:generate
touch database/database.sqlite
php artisan migrate --seed # roles & permissions (+ demo users in non-prod)
php artisan testServe it: php artisan serve (or php artisan octane:start). API docs at http://localhost:8000/docs/api.
Default dev login (seeded only outside production): admin@example.com / password (admin) and
test@example.com / password (user).
docker compose up -dApp available at http://localhost:8080 (Nginx → PHP, with PostgreSQL).
The repo ships with development defaults. Each app built from it must set the following before going live — none of this is done for you:
APP_ENV=productionandAPP_DEBUG=false—APP_DEBUG=trueleaks stack traces & secrets.- A real
MAIL_MAILER(SMTP/SES) — the example useslog, so verification / reset / OTP mail never actually leaves the server otherwise. - Run a queue worker (
php artisan queue:work) — verification and OTP notifications are queued, so without a worker they never send. The Docker image already runs one via Supervisor. - Fill the real secrets in
.env:STRIPE_*,GOOGLE_*/APPLE_*, and lockCORS_ALLOWED_ORIGINS(orFRONTEND_URL) to your real domain — never*. - Deploy steps:
php artisan migrate --force, thenconfig:cache+route:cache.
Demo accounts are never seeded when APP_ENV=production; provision your first admin through a
dedicated flow instead.
All endpoints are prefixed with /api/v1. Interactive docs are generated by Scramble at
/docs/api. Auth column: — public · ✓ Sanctum token · admin requires the admin role.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/register |
Create a new account | — |
POST |
/login |
Obtain a Sanctum token | — |
POST |
/logout |
Revoke the current token | ✓ |
POST |
/forgot-password |
Request a password reset link | — |
POST |
/reset-password |
Complete a password reset | — |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/email/verify/{id}/{hash} |
Verify email via signed URL | — |
POST |
/email/verification-notification |
Re-send the verification email | ✓ |
POST |
/user/email/send-otp |
Send an OTP to set/change the email | ✓ |
POST |
/user/email/verify-otp |
Verify the OTP and apply the email | ✓ |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/auth/{provider}/redirect |
Get the provider redirect URL | — |
GET |
/auth/{provider}/callback |
Handle the OAuth callback (stateless) | — |
{provider} is google or apple.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/user |
Retrieve the authenticated user | ✓ |
PUT |
/user |
Update profile (name, email) | ✓ |
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/subscriptions/plans |
List available plans | — |
GET |
/subscriptions/current |
Current subscription status | ✓ |
POST |
/subscriptions/checkout |
Create a Stripe Checkout session | ✓ |
POST |
/subscriptions/portal |
Get the Stripe billing portal URL | ✓ |
POST |
/subscriptions/cancel |
Cancel the active subscription | ✓ |
POST |
/subscriptions/resume |
Resume a subscription on grace period | ✓ |
POST |
/stripe/webhook |
Stripe webhook handler | — |
| Method | Endpoint | Description |
|---|---|---|
GET |
/admin/health |
Database + cache connectivity check |
GET |
/admin/users |
Paginated list (search, role filter, sort) |
GET |
/admin/users/{id} |
Show a user |
POST |
/admin/users |
Create a user (optional role) |
PUT |
/admin/users/{id} |
Update a user |
DELETE |
/admin/users/{id} |
Delete a user (self-deletion blocked) |
GET |
/admin/roles |
List roles with permissions |
POST |
/admin/roles |
Create a role (optional permissions) |
PUT |
/admin/roles/{id} |
Update name / sync permissions |
DELETE |
/admin/roles/{id} |
Delete a role (admin protected) |
GET |
/admin/permissions |
List all permissions |
Run php artisan route:list --except-vendor for the full, authoritative list.
app/
├── Auth/ # Register, login, password reset, email verification
├── User/ # Profile, email OTP, the User model & policy
├── Admin/ # Admin CRUD for users, roles, permissions
├── OAuth/ # Social login (Google, Apple) via Socialite
├── Subscription/ # Stripe checkout, portal, cancel/resume, plans, webhook
├── Health/ # Health check endpoint
├── Http/ # Base controller + middleware (ForceJson, request logging)
├── Providers/ # App + repository-binding service providers
└── Support/ # ApiResponse envelope · Data\Input typed reader
Each domain holds its own Actions/, Controllers/, Data/, Models/, Requests/,
Resources/, and Repositories/ as needed.
composer test # lint → refactor → types → unit (the full gate)
composer test:unit # Pest only
composer test:coverage # Pest with coverage (min 90%)
composer analyse # PHPStan / Larastan (level max)
composer lint # auto-format with Pint
composer refactor # apply Rector| Command | What it checks |
|---|---|
test:lint |
Pint code style (dry-run) |
test:refactor |
Rector (dry-run) |
test:types |
PHPStan / Larastan at level max |
test:unit |
Pest suite — 110 passing |
.github/workflows/ci.yml runs on every push / PR to main:
- lint — Laravel Pint on PHP 8.4
- test — Pest suite (PHP version matrix)
Released under the MIT License.