
Standardizing Laravel Applications at Northwestern University
Next Article
Northwestern manages a portfolio of Laravel applications, everything from small internal tools to University-wide public-facing websites. The business goals change, but the first stretch of every project tends to repeat itself. This is how I eliminated that repetitive grind by building a production-ready starter that ships with everything we need already wired together.
The Foundational Overhead
Domain logic is unique to every project, but the operational requirements follow a predictable script:
Manual Setup Checklist
0/7If you summoned the effort to click seven checkboxes, kudos. Even that feels like work. Now imagine actually doing all of this: over, and over, and over again.
By the time this foundation is laid, days may have passed without a single line of domain logic written.
Multiply that across a few projects per year, and you're looking at weeks of duplicated effort annually. The work is tedious, but more importantly, error-prone. Configuration drifts across projects. Security implementations vary. Audit requirements are partially implemented. Small inconsistencies compound into maintenance headaches.
The goal was straightforward: eliminate this foundational overhead by packaging our standard stack into a reusable starter that teams can adopt on day one.
What "Production-Ready" Means at Northwestern
Before diving into implementation, let's establish what "production-ready" actually means in Northwestern's context. These aren't some abstract goals, they are hard requirements driven by compliance, security, and operational needs.
Institutional Identity Integration - Northwestern operates a centralized identity system with NetIDs (unique identifiers for students, faculty, and staff), directory services, and SSO infrastructure. Applications can't use Laravel's default email/password authentication. They must integrate with Northwestern's SSO, support via Duo, and provision users automatically from the directory.
Multiple Authentication Methods - Not every user has a NetID. External collaborators, partners, and community members need access to some applications. This means supporting three distinct authentication flows: SSO for internal users, passwordless email-based s for external users, and token-based authentication for service-to-service integrations.
Compliance and Audit Requirements - Many Northwestern applications handle data subject to , , or University policies, requiring attestation of compliance controls. This demands comprehensive audit trails showing who did what, when, and from where. Changes to sensitive records need full before/after tracking. API access needs IP restrictions and detailed logging.
Service-Oriented Architecture - Northwestern's applications don't exist in isolation. They integrate with data warehouses, external SaaS platforms, and other Northwestern services. APIs are considered first-class interfaces. Token management needs to support rotation, expiration notifications, and usage analytics.
Rapid Admin Development - Building admin interfaces consumes disproportionate development time. User management screens, role editors, audit log viewers, and analytics dashboards are table stakes, but they provide no competitive differentiation. The faster we can build these, the more time we spend on actual features.
Collectively, these requirements shaped every architectural decision I made in the starter kit.
The Northwestern Laravel Starter

The Northwestern Laravel Starter is my solution to these requirements: a template repository that consolidates everything into a single foundation. Clone it, configure your environment, run migrations, and you have a functioning application with authentication, authorization, APIs, auditing, and admin tools already operational.
A template isn't a novel idea, and since this one is tailored for Northwestern, the code itself might not apply to you directly. However, the constraints we face - strict compliance, identity management, and integrations - aren't unique to us. This post breaks down how we've solved those problems within Laravel. Even if you never use this starter, the architectural patterns and tradeoffs discussed here are adaptable to any application facing similar requirements.
Dual UI Architecture: Velocity vs. Control
Northwestern requires branded public pages with official colors, typography, and accessibility compliance. However, the administrative interfaces, primarily intended for internal staff, prioritize development velocity over design customization. To support both, the starter employs a dual-stack approach.
Public-Facing UI
- Public content & landing pages
- Complex custom user workflows
- Official Northwestern branding
Administrative UI
- Internal tooling & system config
- Rapid CRUD operations & tables
- Analytics & reporting dashboards
Administrative interfaces are where developers lose the most time. Rather than spending days building user management screens, role editors, and audit log viewers, Filament allows us to fluently build them with minimal code.
Isolating Bootstrap and Tailwind
Running Bootstrap and Tailwind in the same application creates namespace conflicts. A utility class like .container means completely different things in each framework. Tailwind's PurgeCSS scanning can't safely analyze Filament's dynamically generated classes.
The solution is complete asset isolation. Northwestern Laravel UI pages load app.css. Filament panels load theme.css. The two CSS bundles never load on the same page. No namespace collisions, no class conflicts, no mysterious styling bugs.
Deep Dive: How the Asset Isolation Works
Separate Vite Entry Points
We configure Vite to treat the two stacks as completely separate builds.
laravel({
input: [
'resources/sass/app.scss', // Bootstrap + Northwestern branding
'resources/js/app.js', // Public-facing JavaScript
'resources/css/filament/administration/theme.css', // Tailwind + Filament
],
})Each stack compiles independently. Bootstrap never sees Tailwind templates. Tailwind never processes Bootstrap views.
Template Isolation
Public-facing views live in resources/views/ and extend Northwestern Laravel UI layouts:
@extends('northwestern::purple-container')
@section('content')
<div class="container">
<h1 class="page-title">Welcome</h1>
</div>
@endsectionFilament views live in isolated directories (resources/views/filament/, app/Filament/) and only use Tailwind utilities. Filament's panel provider explicitly declares its custom theme to ensure the admin panel never loads Bootstrap assets.
Scoped Tailwind Scanning
Tailwind v4's @source directive explicitly limits class scanning:
@import 'tailwindcss';
@source '../../../../app/Filament/**/*';
@source '../../../../resources/views/filament/**/*';
@source '../../../../vendor/filament/**/*';This tells Tailwind: "Only scan Filament directories." It never touches public views, can't accidentally purge Bootstrap classes, and won't generate conflicting utilities.
What's Included
The Public-Facing UI provides the foundational assets and workflows required for university-compliant user experiences:
- Authentication Flows - Pre-built views for login method selection and passwordless OTP entry.
- Branded Assets - Northwestern-compliant layouts for automated emails (OTP codes, alerts) and HTTP
XXXerror pages. - Component Library - A suite of Blade components for navigation and form elements that adhere to accessibility and branding standards.
The Administrative UI provides a centralized control plane for application operations:
- User Management - View, create, and edit users. Assign roles. Impersonate users. View login history.
- Role & Permission Management - Create roles with granular permissions. Assign users to roles.
- API Management - Create API users, generate tokens, configure IP allowlists, and expiration dates. View request logs and analytics.
- Auditing & Monitoring - Complete audit trail of model changes. HTTP request logging with performance metrics. Real-time analytics dashboards.
Authentication: Three Identity Models
Laravel's default authentication assumes email/password credentials stored in a database.
Our authentication landscape looks completely different:
- Internal users - Authenticate through our identity provider
- External users - Authenticate with email-delivered OTP codes
- Services and integrations - Authenticate with long-lived access tokens
SSO Authentication for Internal Users
Rather than rebuilding SSO integration for every project, the starter uses the northwestern-sysdev/laravel-soa package we already leverage in applications. This package provides first-class support for both Online Passport (agentless SSO with OpenAM/PingIdentity), Microsoft Entra ID, or both simultaneously.
The starter ships preconfigured for Microsoft Entra ID. The package handles session validation, multi-factor authentication with Duo, user provisioning, and secure callback handling. Developers don't need to write authentication code, they just configure which SSO provider to use.
Automatic User Provisioning
The User model schema aligns with Northwestern's directory structure. The authentication flow implements a strategy to ensure the application acts as a downstream consumer of identity data:
- New Users: Upon first login, the user's account is created automatically based on their official directory entry.
- Existing Users: Every login acts as a sync event. If attributes change upstream, those updates are immediately reflected in the application to prevent data drift.
SSO Authentication Flow
Access App
PUSH SENTNo need to deal with manual data entry forms or worry about stale profile data. The application's user remains perpetually in sync with the University's system of record.
NOTE
Caveat: User lifecycle management
Identities change over time (graduation, rehire, reactivation). Our team publishes status updates through our internal ActiveMQ-based broker. For applications that subscribe to the topic, the starter consumes those events via a webhook and automatically revokes roles for locally-known users when their identity enters a deprovisioned state to prevent stale authorization from lingering.
Passwordless Authentication for External Users
Some applications need to support users outside Northwestern's identity system. These users authenticate via email-based OTPs.
To maintain strict governance, external users cannot self-register; they are provisioned exclusively through the admin panel. An administrator enters the user's email address and demographic information (name, affiliation, etc.). The user can then request an OTP delivered to their email.
This keeps authentication simple for external users while maintaining clear administrative control over who has access.
API Authentication for Machine Identities
API authentication requires more control than Laravel Sanctum provides, and far less ceremony than spinning up a full OAuth server with Laravel Passport.
I built a custom token system. This breaks the "never roll your own auth" dogma, but the risk is totally manageable. The full architecture covering token issuance, rotation, and request logging is detailed in API Infrastructure: When to Roll Your Own.
Authorization: The Permission Enum Pattern
We use Spatie's laravel-permission package for role and permission management, but the real challenge is designing a permission structure that's both flexible and maintainable.
Too granular, and you drown in a checkbox hell of user.edit.email vs user.edit.name. Too coarse, and you can't express real authorization requirements.
Centralizing Permissions with Enums
Instead of scattering permission strings throughout the codebase ('view-users', 'manage_users', 'user.view'), all permissions are centralized in an enum:
// Simplified for illustration
enum PermissionEnum: string
{
case VIEW_USERS = 'view-users';
case MANAGE_USERS = 'manage-users';
public function label(): string
{
return match($this) {
self::VIEW_USERS => 'View Users',
self::MANAGE_USERS => 'Manage Users',
};
}
public function description(): string
{
return match($this) {
self::VIEW_USERS => 'View user account details and profiles.',
self::MANAGE_USERS => 'Create and update user accounts.',
};
}
}This eliminates an entire class of bugs. There's no way to accidentally write viwe-users or manage_user if the enum only exposes VIEW_USERS and MANAGE_USERS. Rename a permission, and your IDE refactors every reference automatically. Delete a permission, and your IDE shows every broken reference immediately.
The enum also makes permissions self-documenting. Labels and descriptions live with the permission definition, visible in the admin panel and by developers working in the codebase without additional configuration.
Database Synchronization
While the enum defines permissions in code, they still need to exist as database records for laravel-permission to function. The starter uses idempotent seeding to automatically synchronize enum definitions with database records.
Propagation Through the Stack
Permissions cascade naturally through Laravel's authorization layers.
public function viewAny(User $user): bool
{
return $user->can(PermissionEnum::VIEW_USERS);
}Reference Data Synchronization: Idempotent Seeding
Laravel applications often need reference data. Lookups, statuses, categories, and similar records that exist in enums or concrete code definitions, but require database records to function.
The challenge is keeping your code and the database synchronized across environments without manual intervention.
Traditional Laravel seeders aren't production-safe. Run php artisan db:seed twice and you get duplicate data. Updating a label requires manual database edits in every environment or a migration for the smallest of changes. That is "correct", but it bloats your migration history with noise that has nothing to do with schema changes.
The Solution: Idempotent Seeding
The starter implements an IdempotentSeeder. Instead of a simple UPSERT, it performs full state reconciliation, calculating the difference between your code and the database to create, update, and prune (respecting the model's deletion strategy) records automatically.
You can see this logic in action with the simulator below. Try toggling cases in the enum, then run the seeder to see how the database reacts.
How It Works
The #[AutoSeed] attribute marks seeders for automatic discovery. When you deploy:
- Discovery: The system finds all classes decorated with
#[AutoSeed]. - Dependency Resolution: It builds a directed dependency graph from the
dependsOnlist.- If that graph is acyclic, the resolver uses to produce a topological order.
- If seeders form a loop (), the resolver detects the circular dependency during traversal and fails fast with the full chain.
- Execution: It runs each seeder in the resolved order.
Because this logic is idempotent based on a predetermined slug column, you can include php artisan db:seed --force in your deployment pipeline without fear of duplicating data or causing inconsistencies.
#[AutoSeed(dependsOn: [DocumentTypeSeeder::class])]
class DocumentStatusSeeder extends IdempotentSeeder
{
protected string $model = DocumentStatus::class;
protected string $slugColumn = 'slug';
public function data(): array
{
return collect(DocumentStatusEnum::cases())->map(fn ($s) => [
'slug' => $s->value,
'label' => $s->label(),
])->toArray();
}
}No need to manually modify database records or write one-off migrations. The seeder code is the single source of truth, and rerunning seeders keeps every database in sync automatically.
API Infrastructure: When to Roll Your Own
Northwestern applications live in a service-oriented ecosystem. They integrate with warehouses, SaaS platforms, and other campus systems, so the API foundation has to be operationally solid.
Why Custom Token Management?
Laravel offers two API token paths:
- Passport - When you actually need a full OAuth 2.0 authorization server (clients, grants, refresh tokens, etc.).
- Sanctum - Lightweight token auth / personal access tokens
Both are excellent. Neither matches how Northwestern typically governs cross-application access.
Passport
Passport shines when the application needs to be the OAuth provider. That's not our common shape.
The majority of cross-application requests are proxied through Apigee, with conditional access to APIs and message topics handled centrally through our API Service Registry. In that model, the Laravel app sits behind the gateway and validates an opaque credential, mapping it to a principal with roles and permissions.
Sanctum
Sanctum handles Bearer Auth well, but our requirements push beyond its defaults:
- Principal-centric authorization - API users hold roles with permissions; tokens carry no privileges.
- Network controls - IP and restrictions.
- Lifecycle and observability - expiration alerts, rotation-friendly design, and request analytics.
- Probabilistic Sampling - configurable logging to control storage cost at higher volume.
A purpose-built token system gives end-to-end control over issuance, validation, logging, and revocation, and it aligns with our gateway-first reality.
Token Architecture
The system separates identity from credentials:
- API Users are machine principals with names, roles, and metadata. They never log in via the UI.
- Access Tokens are credentials bound to an API user (optional expiry + optional allowlist) with a stored hash.
The secret is shown once at creation time and typically stored at the edge, encrypted in Apigee . The application stores only the hash for lookup and validation. Multiple tokens per API User enable independent rotation.
Request Logging and Analytics
Every authenticated API request logs:
- Token and user identification
- Unique trace ID for request correlation
- HTTP status code
- Route and endpoint details
- Request duration in milliseconds
- Response size in bytes
- User agent and IP address
- Failure diagnostics when applicable
The administration panel includes an interactive dashboard showing:
- Request volume trends - API usage over time
- Status code distribution - Success vs. error rates
- Slowest endpoints - Performance bottlenecks
- Top endpoints by volume - Most-used APIs
Default Endpoints
The starter ships with minimal, self-contained API routes:
/api/health- Unauthenticated health check/api/v1/me- Authenticated user profile with roles and permissions/api/v1/me/tokens- Token management (list, create, view, delete)
These endpoints are fully functional but intentionally minimal. Real applications create domain-specific endpoints. The foundation (authentication, logging, error handling) is already operational.
Auditing: Two Layers of Accountability
In an enterprise environment, knowing what happened, when it happened, who did it, and what changed is critical. Even when compliance isn't the driver, audit trails prove invaluable for debugging production issues, investigating security incidents, and understanding how data evolved.
The starter implements two layers of auditing: model changes and API request logging.
Model Change Auditing
We use owen-it/laravel-auditing for tracking Eloquent model changes. Every create, update, and delete operation logs complete before/after values.
The standard package functionality is excellent, but the starter extends it to capture additional context:
Authorization Modifications
Changes to role permissions, and the assignment or revocation of roles on users, are critical security events. These are relationship mutations, not direct attribute updates. When you call assignRole(...) or syncPermissions(...), you're writing to pivot tables (e.g. model_has_roles, role_has_permissions) without changing a "tracked" column on a User or Role entity. These many-to-many operations fall outside the default auditing scope.
To make authorization changes auditable, the starter wraps these pivot operations in explicit helpers (assignRoleWithAudit(), syncPermissionsWithAudit()). Each helper takes a snapshot of the full collection before the change, performs the pivot update, then emits a custom audit event containing the after-state plus a structured diff. For roles, we also persist a modification_origin and optional metadata so the audit log can answer "who changed this, from where (UI, automated process, etc.), and why?" instead of just "something happened".
Impersonation Tracking
Changes made while impersonating another user are clearly marked with both the impersonator and the impersonated user. This distinguishes between actions a user took themselves versus actions another user performed on their behalf.
API Trace ID Correlation
Changes made through API requests include the unique trace_id, allowing you to correlate model changes back to specific API calls. If a bulk operation modifies 50 records, you can trace all 50 changes back to the originating API request with a single database query.
Livewire Component Resolution
Eloquent changes originating from Livewire components present a unique observability challenge. In Livewire v2, component updates were routed to /livewire/message/{component-name}, making it immediately clear which component triggered a change. Livewire v3 consolidated all updates to a single /livewire/update endpoint. A sensible architectural decision that simplifies routing and enables request batching, but one that sacrifices visibility.
I extended the auditing system to restore this context by parsing the component name from the Livewire request payload and appending it to the URL stored in audit records (e.g., /livewire/update#user-profile-form).
API Request Logging
Every API request logs basic information: user, timestamp, route, method, IP address, and user agent. This provides a request-level audit trail separate from model changes.
Combined with model auditing, this gives you complete accountability:
User A made a POST request to
/users/123/editat 2:45 PM from IP 192.168.1.1, which updated theold@example.comtonew@example.com.
Lessons Learned: Advice for Building Your Own Starter
Start with Pain Points, Not Features
Don't build a starter kit with only "cool features." Build it to solve actual repeated pain. Every feature in this starter maps to something we rebuilt multiple times across applications at Northwestern.
If you're building a starter, start with a list of necessary tasks and things you rebuild on every project. Those are your features.
Document Your Decisions
Code lives longer than the people who wrote it. Documentation captures why decisions were made, not just what the code does. This prevents future maintainers from second-guessing design choices and reduces accidental regressions.
Make It Easy to Diverge
Not every application needs every feature. Design your starter so teams can remove or replace components without breaking the rest of the system.
The starter uses feature flags extensively:
API_ENABLED=false # Disables entire API system
LOCAL_AUTH_ENABLED=false # Removes passwordless authentication
API_REQUEST_LOGGING_ENABLED=false # Turns off API request loggingTeams that need the default behavior get it immediately. Teams with different requirements can disable what they don't need without breaking dependencies.
Feature flags also document which parts of the system are modular and which are foundational. If disabling something breaks the application, it shouldn't be a feature flag.
Optimize for the 80% Case
80% of Northwestern applications need what's in the starter. The other 20% extend or modify it for edge cases. Don't let that 20% bloat your core.
Keep the starter focused on common patterns. If a feature only applies to one or two applications, it belongs in those applications, not the starter.
Invest in Developer Experience
Small things matter:
- Assistive console commands - Database snapshots, configuration validation, and anything else that minimizes manual processes
- Context-aware exceptions - Custom exceptions with actionable messages
- Clear naming conventions -
PermissionEnum::MANAGE_USERSinstead of'users.manage' - Consistent response formats - Standardized JSON structure across all endpoints
- Helpful defaults - Sensible configuration values that work out of the box
These don't add features, but they dramatically reduce cognitive load and make the difference between a starter that teams embrace and one they work around.
Test Relentlessly
A starter kit with bugs is a liability. Bugs in shared infrastructure multiply across every project using the starter.
Maintain comprehensive test coverage:
- Unit tests for business logic and utilities
- Integration tests for authentication flows and API endpoints
- End-to-end tests for critical user journeys (login, token creation, role assignment)
The starter includes a CI/CD pipeline (GitHub Actions) that runs tests on every commit and blocks merges if tests fail. This catches regressions before they reach projects.
Dogfood It
The best way to validate your starter is to use it yourself. If your team isn't using it for real projects, you're not validating it properly.
Recent applications at Northwestern had the opportunity to adopt this starter kit. When rough edges surface, they're fixed in the project and available for other teams to adopt.
Results and ROI
Building the starter kit took me quite some time...
To be clear: that is not normal. You do not need to spend such an absurd amount of time to build a useful starter kit. That number is inflated by my own borderline-unhealthy obsession of refining the smallest details.
Conversely, over-engineering is exactly where the payoff comes from. I paid the "tax" once, and now every new project starts with a nice level of polish on day one. Make of that number what you will, but don't let it be a deterrent to building your own.
Beyond Time Savings
Saving hours of development time is great on paper, but the real value of a starter kit lies in standardization:
- Consistency - Every application uses the same authentication, authorization, and auditing patterns
- Security - Security improvements can propagate to all projects
- Onboarding - New developers learn one pattern, then apply it everywhere
- Institutional Knowledge - Best practices are codified rather than buried in one application
These benefits are harder to quantify but arguably more valuable than time savings.
Conclusion
Building a starter kit is an exercise in identifying patterns. Not every organization needs this specific stack, but every organization building similar applications repeatedly should codify its patterns.
The starter will never be ✨ perfect ✨. Requirements evolve, Laravel releases new versions, dependencies update, and Northwestern's infrastructure changes. Having a shared foundation means improvements happen in one place. Detailed changelogs let teams incorporate upstream features or fixes at their own convenience.
If you're in an environment repeatedly building similar applications, consider extracting your own starter. Start small (authentication and authorization are good entry points), solve real pain, and iterate based on actual use. The investment compounds faster than you'd expect.
This starter kit is the result of years of iteration, mistakes, and lessons learned alongside the talented individuals at Northwestern IT.
Special Thanks
- Administrative Systems for championing rigorous process, advocating best practices, and publishing the open-source packages we rely on.

- Open Source Maintainers for their tireless work.
- Taylor Otwell for building such a delightful framework.
Help us build the standard
Found a bug? Have an idea for functionality that belongs in the starter? We welcome pull requests.
Contribute on GitHub