Architecture & Stack
A pure Laravel CMS with a Blade/Livewire admin and a JSON-driven content engine — no Filament, no third-party CMS packages.
Technology stack
| Layer | Technology |
|---|---|
| Language | PHP 8.2+ |
| Framework | Laravel 12 |
| Database | MySQL 8.0 (UUID PKs, JSON columns, functional indexes) |
| Admin UI | Blade + Livewire 3 + Alpine.js |
| Styling | Tailwind CSS 3 |
| Rich text | TipTap editor |
| Cache & queue | Redis |
| Auth | Laravel session (admin) + Sanctum / scoped API keys (API) |
| Build tooling | Vite |
Repository layout
app/
Models/ — Eloquent models (UUID-keyed)
Http/
Controllers/Admin/ — Admin panel controllers
Controllers/Api/V1/ — REST API controllers
Requests/ — Form request validators
Middleware/ — api.auth, api.cors, api.rate, api.scope, api.log
Observers/ — Activity logging, revision creation
Traits/HasUuid.php — UUID primary keys
database/
migrations/ — Schema definitions
seeders/ — Default roles, settings, admin user
resources/
views/admin/ — Blade templates per module
views/layouts/ — Shared admin layout
routes/
web.php — Admin panel routes (admin.* names)
api.php — REST API v1 (prefix: /api/v1)
Architectural decisions
Everything is a content model
Pages, menus, headers, footers, and other site primitives are not separate modules. They are content models the editor defines through the admin. Combined with components and (planned) dynamic zones, this keeps the codebase small while letting site editors model arbitrary content.
pages, menus, or blocks table. Look for those concepts under content_models and content_entries.UUID primary keys everywhere
All tables use 36-character UUID primary keys via the HasUuid trait. Route model binding uses UUIDs directly, which makes URLs opaque and prevents enumeration.
JSON-first field storage
Each entry stores its field values in a single data JSON column, governed by the entry's content model. This avoids per-model schema migrations and lets editors evolve their models without DBA involvement. A MySQL 8 functional index on data->>'$.title' keeps listing queries fast.
Separate admin and API surfaces
routes/web.php contains all admin functionality behind session auth and the admin.auth middleware. routes/api.php is the public-facing REST surface, all under /api/v1, gated by API keys with scopes (read, write, delete), per-key rate limits, and CORS origin allowlists.
Observers handle audit trail & revisions
Every meaningful save flows through a model observer. Observers write to activity_logs and create content_revisions rows so editors can diff and restore prior versions of any entry.
Routing conventions
- Admin routes use route model binding with UUIDs. Names are prefixed with
admin.. - Entries are scoped under their content model slug:
/admin/content/{contentModel:slug}/entries/{entry}. - Single-type entries have a shortcut:
/admin/content/{contentModel:slug}/single. - API routes are flat under
/api/v1and bound by content model slug.
Naming & file conventions
- Models in
app/Models/— one file per Eloquent model. - Admin controllers in
app/Http/Controllers/Admin/. - API controllers in
app/Http/Controllers/Api/V1/, suffixedApiController. - Form requests in
app/Http/Requests/. - Blade views in
resources/views/admin/<module>/.
Development commands
# Full dev stack (server, queue, logs, Vite)
composer dev
# Just the PHP server
php artisan serve
# Reset DB with seeds
php artisan migrate:fresh --seed
# Vite asset watcher
npm run dev
# Run tests
composer test