These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

Backend — auth and roles

JWT issuance, role gating, and subscription-tier gating — all middleware-driven.

For the higher-level JWT flow (including frontend token storage and the 401 redirect path), see Architecture — Auth.


JWT issuance (tymon/jwt-auth)

Package: tymon/jwt-auth ^2.1, registered via config/jwt.php.

Algorithm: HS256 (default via JWT_ALGO; override with env var) — config/jwt.php:134

Token lifetime: ttl = 20160 minutes (14 days) — config/jwt.php:104

Refresh TTL: JWT_REFRESH_TTL (default 20160 min, 14 days) — config/jwt.php:123

Blacklist: enabled by default (JWT_BLACKLIST_ENABLED=true) — config/jwt.php:220. Tokens are added to the blacklist on POST /logout. Grace period defaults to 0 seconds (JWT_BLACKLIST_GRACE_PERIOD) — config/jwt.php:235.

Secret: JWT_SECRET (HMAC symmetric) — config/jwt.php:28. Generate with php artisan jwt:secret.

The api guard

config/auth.php:43-46 registers an api guard with driver: jwt and provider: users. Every protected route uses the auth:api middleware, which resolves the JWT and hydrates $request->user() with the authenticated User Eloquent model.

JWTSubject implementation

app/Models/User.php:12User implements JWTSubject:

MethodReturns
getJWTIdentifier()$this->getKey() (primary key) — User.php:42
getJWTCustomClaims()['role', 'subscription_status', 'subscription_tier']User.php:46-53

The three custom claims are embedded in every issued token, so middleware can read them without a database round-trip. However, the middleware implementations shown below intentionally re-read the live model values from $request->user() (which is already database-loaded by the guard) to avoid acting on stale JWT claims after an account change.

Token issuance flow

  1. POST /api/loginAuthController::login validates credentials, calls auth()->attempt() which invokes tymon/jwt-auth and returns a signed bearer token.
  2. Token is returned in the response body alongside toAuthPayload() (user fields + permissions array — User.php:210-231).
  3. Every subsequent request carries Authorization: Bearer <token>. The auth:api middleware validates, refreshes if needed, and sets $request->user().

Password hashing

password column is cast to hashed in User::casts()User.php:109. Laravel’s default bcrypt driver applies APP_BCRYPT_ROUNDS (defaults to 12 in .env.example:15).


Role system

Constants

Defined on Userapp/Models/User.php:17-18:

ConstantValueMeaning
ROLE_ADMIN'admin'Full admin console access; bypasses all subscription-tier checks
ROLE_USER'user'Standard customer account; default on registration

Default at creation: ROLE_USERUser.php:35.

EnsureUserRole middleware

app/Http/Middleware/EnsureUserRole.php — alias role, registered in bootstrap/app.php:25.

// EnsureUserRole.php:11-19 public function handle(Request $request, Closure $next, string ...$roles): Response { $user = $request->user(); if (!$user || !in_array($user->role, $roles, true)) { return response()->json(['message' => 'You do not have permission to access this resource.'], 403); } return $next($request); }

Usage in routes: 'role:admin'. Multiple roles are passed as additional variadic arguments (e.g. role:admin,moderator), though only admin and user currently exist in the codebase.

Admin route group

routes/api.php:156-204 — all /users CRUD and every /admin/* route are wrapped in middleware('role:admin'). The /admin/* prefix covers:

  • AI log management (GET/POST/DELETE /admin/ai-logs)
  • Audit log browsing (GET /admin/audit-logs, GET /admin/audit-logs/{auditLog})
  • User, workspace, location, finder, set, and tag management under /admin/

Admin invite sending (POST /invite/send) uses its own standalone middleware group at routes/api.php:53.

Checking role in application code

User::isAdmin()User.php:163 — returns $this->role === self::ROLE_ADMIN.

User::permissions()User.php:199 — returns ['admin.access', 'users.manage'] for admins, empty array for others. The permissions array is included in toAuthPayload() and sent to the frontend after login.


Subscription-tier system

Constants

Defined on Userapp/Models/User.php:23-27:

ConstantValueRank
SUBSCRIPTION_TIER_NONE'none'0
SUBSCRIPTION_TIER_FREE'free'0
SUBSCRIPTION_TIER_BRONZE'bronze'1
SUBSCRIPTION_TIER_PREMIUM'premium'2
SUBSCRIPTION_TIER_CUSTOM'custom'3 (bypasses rank check)

Subscription status constants — User.php:20-21:

ConstantValue
SUBSCRIPTION_STATUS_UNPAID'unpaid'
SUBSCRIPTION_STATUS_PAID'paid'

Default at creation: subscription_status = 'unpaid', subscription_tier = 'free'User.php:36-37.

EnsureSubscriptionTier middleware

app/Http/Middleware/EnsureSubscriptionTier.php — alias subscription_tier, registered in bootstrap/app.php:26.

// EnsureSubscriptionTier.php:11-19 public function handle(Request $request, Closure $next, string ...$tiers): Response { $user = $request->user(); if (!$user || !$user->canAccessSubscriptionTiers($tiers)) { return response()->json(['message' => 'This feature requires a qualifying subscription.'], 403); } return $next($request); }

Usage in routes: 'subscription_tier:free', 'subscription_tier:bronze', etc. Multiple tiers are passed as variadic args; the middleware checks whether the user’s tier rank is at least as high as the highest-ranked tier in the list.

canAccessSubscriptionTiers() logic

User.php:177-196 — the authoritative tier-check method:

  1. Admin bypass — if isAdmin() returns true, access is granted unconditionally.
  2. Custom tier bypasssubscription_tier === 'custom' always passes.
  3. Unpaid check — if not hasPaidSubscription() and tier is not 'free', access is denied.
  4. Rank comparison — fetches the current rank from subscriptionTierRanks() and the required rank (max of all passed tier ranks); passes if currentRank >= requiredRank.

Tier usage in routes

The workspace CRUD group is the only route group currently gated by subscription_tier:freeroutes/api.php:147-153. This grants access to all users at the free tier or above (which in practice means all authenticated users, since free rank is 0).

⚠️ Warning: The subscription_tier:free gate is rank-0, so it is effectively a paid-status gate for tiers above free. Any new feature requiring a paid subscription should use subscription_tier:bronze (rank 1) or subscription_tier:premium (rank 2).

Middleware registration

bootstrap/app.php:23-28 — alias table:

AliasClass
auth (built-in)Laravel’s Authenticate
auditApp\Http\Middleware\RecordAuditLog
roleApp\Http\Middleware\EnsureUserRole
subscription_tierApp\Http\Middleware\EnsureSubscriptionTier
auth_sessionApp\Http\Middleware\TrackAuthSession

Middleware composition order

Every protected route group uses: auth:apiauth_sessionauditroutes/api.php:34. Then, within nested route groups, role:admin or subscription_tier:<tier> is appended as an inner middleware.

Full stack for an admin route: auth:apiauth_sessionauditrole:admin.

For the audit middleware detail, see Backend — audit logging. For the audit header protocol sent by the frontend, see Next.js → Backend integration.