livenetworks / ln-starter
Laravel foundation package — base controllers, models, DTOs, and middleware for dual-mode (browser + API) applications.
Requires
- php: ^8.3
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/mail: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- laravel/sanctum: ^4.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v1.2.1
- v1.2.0
- v1.1.1
- v1.1.0
- 1.0.0
- dev-claude/step-8-consumer-adoption
- dev-claude/step-5-release-candidate
- dev-claude/step-4-production-qualification
- dev-claude/step-3-security-observability
- dev-codex/step-1-review-fixes
- dev-codex/step-2-auth-state-machine
- dev-codex/step-1-security-foundation
This package is auto-updated.
Last update: 2026-09-28 23:30:12 UTC
README
Laravel foundation package by Live Networks. Base classes and conventions for building dual-mode (browser + API) Laravel applications.
Laravel 12 and 13 are the supported deployment targets and are gated on
composer audit. Laravel 11 remains only as an end-of-life compatibility lane
so existing applications can still install the package; a green Laravel 11 lane
proves installability, not that Laravel 11 is security-supported. Do not deploy
it as a production target.
MySQL/MariaDB (InnoDB) and PostgreSQL are supported databases. SQLite is for development and unit tests only — auth v2 refuses it in production because it cannot provide the row locking that single-use proof consumption depends on.
See docs/deployment.md and
ADR 0003.
Core principle
One URL, one controller, one logic — output adapts to the request type.
A single route serves both the browser (HTML via Blade) and API clients (JSON). The controller writes business logic once; the response layer inspects the Accept header and X-Requested-With to decide how to render the result.
POST /members
├── Browser form submit → HTML redirect / Blade view
├── AJAX (XMLHttpRequest) → JSON with rendered Blade sections
└── API (Accept: application/json) → pure JSON payload
Installation
composer require livenetworks/ln-starter
The service provider auto-registers via Laravel package discovery.
Publish assets
# Config (includes auth settings) php artisan vendor:publish --tag=ln-starter-config # Layouts (_app, _ln, _ajax, _auth → resources/views/layouts/) php artisan vendor:publish --tag=ln-starter-layouts # Auth views & email templates (override package views) php artisan vendor:publish --tag=ln-starter-views # Auth SCSS (publishes to resources/scss/auth.scss) php artisan vendor:publish --tag=ln-starter-auth-css # Auth v2 migration (magic_login_attempts) php artisan vendor:publish --tag=ln-starter-migrations # Optional Sanctum PAT migration (not used by built-in auth) php artisan vendor:publish --tag=ln-starter-sanctum-migrations # Stubs (for scaffolding new controllers/models) php artisan vendor:publish --tag=ln-starter-stubs # Claude AI skill (publishes to .claude/skills/ln-starter/) php artisan vendor:publish --tag=ln-starter-skill
Package contents
ln-starter/
├── src/
│ ├── Http/
│ │ ├── LNController.php # Base controller with dual-mode response
│ │ ├── Controllers/
│ │ │ └── AuthController.php # Passwordless auth (magic link)
│ │ └── Middleware/
│ │ ├── AuthenticateWithSanctum.php # Bearer token auth
│ │ ├── AuthorizationFromCookie.php # Cookie→Bearer bridge
│ │ ├── DisableCsrf.php # CSRF skip marker
│ │ └── VerifyCsrfToken.php # CSRF with route-aware skip
│ ├── View/
│ │ └── LNViewComposer.php # Base view composer
│ ├── Models/
│ │ ├── LNReadModel.php # Read-only Eloquent (DB views)
│ │ ├── LNWriteModel.php # Write Eloquent (no timestamps)
│ │ ├── MagicLoginAttempt.php # Auth v2 attempt state
│ │ └── MagicLinkToken.php # Legacy cleanup compatibility
│ ├── Jobs/
│ │ └── ProcessMagicLoginRequest.php # Encrypted async mail job
│ ├── Mail/
│ │ └── MagicLinkMail.php # Magic link email
│ ├── DTOs/
│ │ └── Message.php # Unified response message
│ ├── Exceptions/
│ │ └── BusinessException.php # Domain-level exception
│ └── LnStarterServiceProvider.php # Package service provider
├── config/
│ └── ln-starter.php # Package configuration
├── routes/
│ └── auth.php # Auth routes (loaded when enabled)
├── database/
│ └── migrations/
│ └── auth-v2/create_magic_login_attempts_table.php
├── resources/
│ ├── scss/
│ │ └── auth.scss # Auth styles (BEM, ln-acme mixins)
│ └── views/
│ ├── layouts/
│ │ ├── _ln.blade.php # Layout switcher (AJAX vs full page)
│ │ ├── _ajax.blade.php # JSON response layout for AJAX
│ │ └── _auth.blade.php # Minimal auth layout
│ ├── auth/
│ │ ├── login.blade.php # Login form (magic link)
│ │ ├── magic_code.blade.php # Six-digit code form
│ │ └── magic.blade.php # Token-free link confirmation
│ └── emails/
│ └── magic-link.blade.php # Magic link email template
├── docs/
│ ├── auth.md # Auth module setup & flow
│ ├── auth-v2-test-matrix.md # Security acceptance matrix for auth v2
│ ├── security-logging.md # Security events, sinks, retention
│ ├── deployment.md # Deploy checklist, runbook, rollback
│ ├── adr/
│ │ ├── 0001-magic-link-authentication-v2.md # Accepted auth v2 design
│ │ ├── 0002-security-audit-logging-and-observability.md # Audit pipeline
│ │ └── 0003-auth-v2-production-and-release-contract.md # Support matrix
│ ├── dual-mode-response.md # How the response system works
│ ├── read-write-models.md # Read/write model separation
│ ├── message-dto.md # Message DTO usage
│ ├── middleware.md # Middleware reference
│ ├── view-composers.md # View composer pattern
│ └── conventions.md # Naming and architecture conventions
├── stubs/
│ ├── controller.stub # Controller scaffold
│ ├── read-model.stub # Read model scaffold
│ └── write-model.stub # Write model scaffold
├── composer.json
├── CLAUDE.md # AI instructions (lean, for Claude Code)
├── skills/
│ └── ln-starter/
│ └── SKILL.md # Claude AI skill (detailed, publishable)
├── CHANGELOG.md
└── LICENSE
Architecture
LNController
Every controller extends LNController. The key method is respondWith():
class MemberController extends LNController { public function index() { $members = VMember::all(); return $this->view('members.index') ->respondWith($members); } public function store(Request $request) { $member = Member::create($request->validated()); return $this->view('members.index') ->respondWith( $member, new Message('success', 'Created', 'Member created successfully.') ); } }
How respondWith() decides the output:
| Condition | Output |
|---|---|
Accept: application/json (not AJAX) |
Pure JSON: { message, content } |
X-Requested-With: XMLHttpRequest |
Blade view through _ajax layout → JSON with rendered sections |
| Regular browser request | Full Blade view through _app layout |
The _ln.blade.php layout handles the AJAX/full-page switch:
@extends(request()->header('X-Requested-With') === 'XMLHttpRequest' ? 'layouts._ajax' : 'layouts._app')
LNViewComposer
Base class for view composers. Provides access to the $response['content'] array that respondWith() populates. All composers follow the same pattern: read response data, enrich it, write it back.
class MemberFormComposer extends LNViewComposer { public function enrich(array &$content, View $view): void { $content['lodges'] = Cache::remember('lodges.all', INF, fn() => Lodge::all() ); $content['grades'] = Cache::remember('grades.ordered', INF, fn() => Grade::orderBy('sequence')->get() ); } }
The base class handles the boilerplate of extracting $response['content'] from the view data and writing it back. Your composer only implements enrich().
Register composers in a service provider:
View::composer('members.form', MemberFormComposer::class); View::composer('members.index', MembersIndexComposer::class);
Read/Write model separation
LNReadModel— for database views and read-only tables. All write operations (create,update,delete,save) returnfalse. No timestamps.LNWriteModel— standard Eloquent model for write operations. No timestamps by default (override in child if needed).
// Read-only: backed by a DB view class VMember extends LNReadModel { protected $table = 'v_members'; } // Writable: backed by a real table class Member extends LNWriteModel { protected $table = 'members'; protected $fillable = ['name', 'email', 'lodge_id']; }
Message DTO
Unified message object for all response types. JSON-serializable.
$message = new Message( type: 'success', // success | error | warning | info title: 'Created', body: 'Member was created successfully.', data: ['id' => 42] // optional extra data );
Middleware
| Middleware | Purpose |
|---|---|
AuthenticateWithSanctum |
Validates bearer tokens from Authorization header |
AuthorizationFromCookie |
Deprecated legacy cookie-to-bearer bridge; not used by auth v2 |
DisableCsrf |
Bearer-only marker middleware (disable-csrf:bearer) |
VerifyCsrfToken |
Extended Laravel CSRF that exempts only marked requests with an explicit bearer header |
BusinessException
Domain-level exception for business rule violations. Carries a title and translatable message.
throw new BusinessException( message: 'Member already exists with this email.', title: 'Duplicate', code: 409 );
Configuration
After publishing, edit config/ln-starter.php:
return [ // Layout for full-page requests (your app provides this) 'layout' => 'layouts._app', // Layout for AJAX requests (provided by package) 'ajax_layout' => 'layouts._ajax', // Middleware aliases registered by the package 'middleware_aliases' => [ 'sanctum.token' => \LiveNetworks\LnStarter\Http\Middleware\AuthenticateWithSanctum::class, 'cookie.auth' => \LiveNetworks\LnStarter\Http\Middleware\AuthorizationFromCookie::class, 'disable-csrf' => \LiveNetworks\LnStarter\Http\Middleware\DisableCsrf::class, ], // Passwordless auth (magic link) 'auth' => [ 'enabled' => false, // opt-in 'user_model' => 'App\\Models\\User', 'token_expiry' => 15, // minutes 'home_route' => 'home', // route name after login 'mail_subject' => 'Magic Link Login', 'layout' => 'layouts._auth', ], ];
Auth module (Passwordless / Magic Link)
The package includes an opt-in passwordless authentication module using magic links. Disabled by default.
Setup
1. Enable in config
// config/ln-starter.php 'auth' => [ 'enabled' => true, // ... ],
2. Publish auth styles and add to Vite
php artisan vendor:publish --tag=ln-starter-auth-css
This copies auth.scss to resources/scss/auth.scss. Add it to your vite.config.js input array:
export default defineConfig({ plugins: [ laravel({ input: [ 'resources/scss/auth.scss', // ← add this 'resources/scss/app.scss', 'resources/js/app.js', ], }), ], });
Then build:
npm run build
The auth SCSS is fully standalone — no
ln-acmeor other npm peer dependency required. All styles (custom properties, reset, animations, BEM components) are self-contained.
The auth pages render before any of this. The shipped layout asks your application's own Vite service for the tags — so a custom hot file, build directory or manifest name is honoured — and emits nothing when the entries have not been built. A fresh install, or a deploy where the asset build was skipped, gets an unstyled but working login page rather than HTTP 500 on the entry point of the auth flow.
3. User model prerequisites
Your User model must:
- Implement Laravel's
Authenticatablecontract (the normal Laravel user model) - Expose its canonical email address
class User extends Authenticatable { protected $fillable = ['email']; }
4. Run migrations
php artisan migrate
This creates the additive magic_login_attempts table. The migration is loaded automatically when auth is enabled. To publish it for customization:
php artisan vendor:publish --tag=ln-starter-migrations
5. Configure the auth-v2 pepper, queue, and session
LN_AUTH_PEPPER_ID=v1 LN_AUTH_PEPPER=base64:REPLACE_WITH_AT_LEAST_32_RANDOM_BYTES QUEUE_CONNECTION=database SESSION_DRIVER=database
Run an asynchronous queue worker in production. Built-in auth uses only the
Laravel web session; it does not require HasApiTokens, an auth_token
cookie, or the cookie-to-header middleware.
6. Audit upgrades from auth v1
php artisan ln-starter:auth-v2-audit php artisan ln-starter:auth-v2-readiness php artisan ln-starter:auth-v2-cutover --force
Routes registered
| Method | URI | Name | Purpose |
|---|---|---|---|
| GET | /login |
login |
Login form |
| POST | /auth/magic-link |
login.magic-link |
Send magic link email |
| GET | /auth/magic/code |
auth.magic.code.form |
Six-digit code form |
| POST | /auth/magic/code |
auth.magic.code |
Consume code in requesting session |
| GET | /auth/magic/{token} |
auth.magic.link.open |
Exchange URL proof for token-free context |
| GET | /auth/magic/confirm/{context} |
auth.magic.link.confirm |
Show confirmation page |
| POST | /auth/magic/confirm/{context} |
auth.magic.link.consume |
Consume link and authenticate session |
| POST | /logout |
logout |
Invalidate session and redirect to login |
Flow
1. User enters an email; the public response is identical for every address.
2. An encrypted queue job checks eligibility and sends a link plus six-digit code.
3. The requesting browser can enter the code; the link can be opened elsewhere.
4. Link GET exchanges the URL secret for a bounded session context and redirects.
5. A CSRF-protected POST consumes either proof, starts a web session, and revokes siblings.
Render a CSRF-safe logout form with:
<x-ln.logout-form class="nav-logout">{{ __('Sign out') }}</x-ln.logout-form>
Why two steps? Email scanners pre-fetch URLs via GET. GET never authenticates; only the explicit, CSRF-protected confirmation POST consumes the proof.
Customizing views
Publish and override:
php artisan vendor:publish --tag=ln-starter-views
Views are published to resources/views/vendor/ln-starter/. Edit:
auth/login.blade.php— login formauth/magic_code.blade.php— code entryauth/magic.blade.php— token-free magic link confirmationemails/magic-link.blade.php— link plus code emaillayouts/_auth.blade.php— auth page layout
Or point config('ln-starter.auth.layout') to your own layout.
Translating
All user-facing strings use __(). Publish Laravel lang files and translate as needed.
Project-specific extensions
The package provides the foundation. Your project adds:
- App layout (
_app.blade.php) — sidebar, header, footer, project-specific assets - Auth layout (
_auth.blade.php) — login/register pages - RBAC middleware (
CheckPermission,CheckRole) — domain-specific authorization - Domain middleware (
EnsureMemberExists) — domain-specific context - Form Requests — validation is always project-specific
- View Composers — extend
LNViewComposer, implementenrich()to add view-specific data - Traits — reusable controller behaviors (pagination, filtering)
AI skill
The package includes a Claude AI skill for code generation. After publishing:
php artisan vendor:publish --tag=ln-starter-skill
The skill is copied to .claude/skills/ln-starter/SKILL.md. Claude will read it automatically and generate code that follows LN-Starter conventions — correct base classes, dual-mode response pattern, read/write model separation, view composer pattern, etc.
License
MIT © Live Networks