My User model has role in $fillable. It has to — my seeders create admin accounts and the admin panel updates roles. So mass-assignment protection does not save me here. Anyone can POST a role field to my register endpoint, and Eloquent would happily accept it — if I ever passed request data straight through.
I don't. The register method hardcodes it:
public function register(RegisterRequest $request): JsonResponse { $user = User::create([ 'name' => $request->validated('name'), 'email' => $request->validated('email'), 'password' => $request->validated('password'), 'role' => 'user', // never from the request ]); $token = $user->createToken('auth-token')->plainTextToken; return response()->json([ 'user' => new UserResource($user), 'token' => $token, ], 201); } Two layers, both deliberate:
1. The FormRequest has no role rule. RegisterRequest validates name, email (unique) and password (min 8, confirmed). A role key in the payload never even survives validation — validated() drops it.
2. The controller sets the value explicitly. Even if validation passed it through, the create call ignores the request for this column. There is exactly one place in the codebase that decides a new signup's role, and it is that one line.
Why both? Defence in depth sounds like a slogan until someone refactors the FormRequest six months later and adds a field. The controller line is the real lock; the FormRequest is the habit that keeps the payload surface small.
Elevated roles get created by seeders and the admin panel only (the admin update route validates role against in:admin,user and is itself guarded by the role middleware plus an isAdmin() authorisation check). Registration creates customers. That rule is enforced in code, not in documentation.
This is one of the small security decisions baked into the Laravel + React SaaS Starter Kit — Sanctum auth, role middleware and the user management loop come wired this way out of the box: https://kamranofficial.gumroad.com/l/mhekoig
![Why Object.keys(user) Returns string[], Not (keyof User)[]: TypeScript Means It](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q4w48b9bzs43tynyhsf.png)
