Laravel vs. Next.js: 897 Lines of Server Actions Where Eloquent Would Live
No ORM, no Artisan, no PHP anywhere — 271 lines of SQL and 897 lines of Server Actions doing what Laravel's migrations and controllers do instead.
Laravel is a PHP framework that bundles routing, an ORM, an admin-friendly console, and authentication scaffolding into one full-stack package. Next.js is a React framework for rendering and routing; it has no ORM, no CLI code generator, and no built-in auth system, so "the backend" is whatever you build with Server Actions, Route Handlers and a database client. This repository is the second kind: zero PHP anywhere, 271 lines of SQL doing what Eloquent migrations do, and 897 lines of TypeScript doing what Laravel controllers do.
What "backend" means on each side
Laravel ships Eloquent (an ORM with model classes and relationships), Artisan (php artisan make:migration, make:controller, make:model — code generators for all of it), a request/controller/route pipeline, and Sanctum or Breeze for authentication, all in the framework's own conventions. Next.js ships none of that. It gives you file-based routing, Server Components, and two ways to run server code — Server Actions and Route Handlers — and leaves the database, the ORM decision, and the auth system entirely up to you. Django's version of the same gap is even starker, because the admin panel it hands you for free has no equivalent here at all.
$ find . -maxdepth 3 -iname "*.php" | grep -v node_modules
$
No output. This isn't a migration story — this storefront was Next.js from its first commit — but it's the honest starting point for the comparison: everything below is what a team builds when the framework doesn't build it for them.
Migrations: the same word, two different tools
Laravel migrations are PHP classes with up()/down() methods, run through php artisan migrate, and are usually paired with Eloquent models that map each table to a class. This codebase's migrations are plain SQL, run through the Supabase CLI, with no ORM layer sitting on top of them at all:
$ ls supabase/migrations/
0001_commerce_init.sql
0002_display_name_allowlist.sql
0003_commerce_rpcs.sql
0004_lock_down_rate_limit_fn.sql
0005_retain_anonymized_download_audit.sql
0006_pricing_v2_slot_kinds.sql
0007_atomic_download_rate_limit.sql
0008_refund_requests.sql
$ wc -l supabase/migrations/*.sql | tail -1
271 total
Eight files, 271 lines, hand-written Postgres — create table, alter table ... enable row level security, create policy. A Laravel migration for the same schema would generate broadly similar DDL underneath, but through Schema::create()'s fluent builder rather than raw SQL, and it would expect an Eloquent model on the other side translating rows to objects. Nothing here does that translation. Every query in this codebase — Server Action or Route Handler — calls the Supabase client directly and gets back plain rows typed by hand, not model instances with relationship methods attached.
Where Eloquent's job goes instead
A Laravel app puts write logic in a controller method, usually backed by a model's save() or update(). This app puts it in a "use server" function:
$ grep -rl '"use server"' src/lib/actions/*.ts
src/lib/actions/account.ts
src/lib/actions/auth.ts
src/lib/actions/checkout.ts
src/lib/actions/contact.ts
src/lib/actions/entitlementsView.ts
src/lib/actions/newsletter.ts
src/lib/actions/redemption.ts
src/lib/actions/refund.ts
$ grep -c '^export async function' src/lib/actions/*.ts | awk -F: '{sum+=$2} END {print sum}'
18
$ wc -l src/lib/actions/*.ts | tail -1
897 total
Eight files, 18 exported functions, 897 lines — this codebase's entire mutation surface, called directly from Server Components with no route or controller class in between. redeemSlot() in redemption.ts does what a Laravel RedemptionController@store method plus an Eloquent transaction would: validates the input, checks the caller owns the entitlement, writes the update, all in one function with no framework-mandated layering around it. There's no RedemptionRequest form-request class, no RedemptionController, no Redemption model — a Laravel team would write three files where this one writes one function.
Two Route Handlers exist for the cases a Server Action can't cover — a webhook needs a stable URL a third party can POST to, and a signed-download endpoint needs to return a redirect rather than serialized data:
$ find src/app/api -name "route.ts"
src/app/api/webhooks/lemonsqueezy/route.ts
src/app/api/download/route.ts
Two files. A Laravel equivalent would route both through routes/api.php to controller methods; the shape is comparable, the ceremony around it isn't.
What Laravel gives you that this stack doesn't
| Laravel | This codebase | |
|---|---|---|
| ORM / model layer | Eloquent — model classes, relationships, query builder | None — direct Supabase client calls, hand-typed rows |
| Code generation | artisan make:* scaffolds migrations, controllers, models, requests | None — every file is written from scratch |
| Auth scaffolding | Breeze/Sanctum/Fortify — ships as a package | Hand-built (src/lib/actions/auth.ts), against Supabase Auth directly |
| Admin/back-office UI | Nova (paid) or community packages generate CRUD screens from models | Hand-built dashboard (src/app/dashboard/, 3 files, 556 lines) |
| Authorization | Policy classes + gates, framework-integrated | Postgres row-level security (see this schema's RLS posture) |
| Where "write" logic lives | Controllers, form requests, model events | Server Actions, one function per operation |
| Database portability | Laravel's query builder abstracts several SQL dialects | Supabase-specific: RPCs, RLS policies and Storage calls, all Postgres |
The honest reading of this table isn't "Next.js is missing features" — it's that Laravel bundles opinions Next.js deliberately leaves unbundled. A team that wants Eloquent's conventions, Artisan's generators, and a back-office UI out of the box gets real, non-trivial value from Laravel that this stack doesn't offer. A team assembling a Postgres-backed storefront with row-level security as the authorization layer — this codebase's actual choice — ends up writing the 897 lines above instead of learning Eloquent's, and keeps direct control over exactly what every write path does, including the one race condition (the F-1 download-limit fix) that got fixed at the SQL level rather than inside a framework's model-event system.
Mistakes going either direction
| Symptom | Cause | Fix |
|---|---|---|
Porting a Laravel model's $fillable/$guarded habit to a Server Action | Mass-assignment protection is an Eloquent concept; Server Actions have no equivalent unless you write it | Validate and allowlist fields explicitly inside the action, the way src/lib/actions/account.ts does per field |
Expecting a Next.js Route Handler to auto-wrap responses like Laravel's Response macros | Next.js Route Handlers return a plain Response/NextResponse — no framework response transformation | Build the response shape by hand, or write a small shared helper if the pattern repeats |
| Assuming RLS is "Laravel policies, but in the database" | RLS evaluates per-row, per-SQL-statement, independent of anything in the app layer; a Laravel policy only runs when a controller calls authorize() | Treat RLS as a backstop that runs even if the app layer has a bug, not a drop-in replacement for explicit checks in Server Actions |
Looking for an Eloquent-style Model::find() and not finding one | This codebase never introduced an ORM | Query the Supabase client directly and type the row shape by hand, as every action file here does |
| Expecting migrations to know about model relationships | Laravel migrations are commonly authored alongside Eloquent models that encode hasMany/belongsTo | This schema's relationships are just foreign keys (references auth.users(id)); nothing generates or enforces them beyond the SQL constraint itself |
Frequently asked questions
Could this codebase have been built in Laravel instead?
Structurally, yes — Postgres, row-level security and webhook-driven order creation aren't Next.js-specific ideas. The costs would move, not disappear: Eloquent and Artisan would remove some of the 897 lines of hand-written Server Actions, but the framework doesn't know about Supabase's storage buckets, signed URLs or SECURITY DEFINER RPCs, so that half would still be custom code either way.
Does Next.js have anything like Artisan's code generation?
Not built-in. The closest analogues are third-party CLI tools (create-next-app for scaffolding a whole project, various ORM CLIs like Prisma's or Drizzle's for schema-first generation) — none of them ship with the framework the way Artisan ships with Laravel, and this codebase uses none of them, writing both the SQL and the TypeScript by hand.
Is Postgres row-level security a fair swap for Laravel's policy classes?
They solve overlapping but not identical problems. A Laravel policy is application code that runs when a controller calls it — skip the call, skip the check. An RLS policy runs inside Postgres itself for every query that touches the table, application code or not. This schema leans on that distinction directly: zero INSERT policies exist on any table, so a write attempted outside the server's SECURITY DEFINER functions fails at the database, not at a layer a bug could bypass.
Which one is faster to build a CRUD admin panel in? Laravel, decisively, if a generated back office is acceptable — Nova or a community package can produce CRUD screens from Eloquent models with little custom code. This codebase's dashboard is hand-built for a narrower job (viewing entitlements and redeeming slots, not general CRUD), and it shows: three files, 556 lines, purpose-built rather than generated.
Does this codebase's lack of an ORM show up anywhere as duplicated code?
Yes — an interface named SlotRow, describing the same entitlement_slots row shape, is declared independently in five different files (src/app/api/download/route.ts, src/app/dashboard/page.tsx, src/lib/actions/entitlementsView.ts, src/lib/actions/redemption.ts, src/lib/actions/refund.ts). An Eloquent model would define that shape once; this codebase re-derives it at every call site instead, which is the direct cost of skipping the model layer — more places to keep in sync by hand if the schema changes.
Templates in this post
ASoc Brief (a resume & portfolio site), ASoc Byte (an IT company website) and ASoc Canvas (a no-code page-builder landing page) are all Next.js editions built the way this post describes — no ORM, no PHP, Server Actions doing the write work.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the database half of this stack in more depth, see Supabase vs Firebase for a Template-Based SaaS; for the framework-choice question from the frontend side, Next.js vs. Angular.
