Skip to main content
ASoc
Comparison

Next.js vs. Django: No Admin Panel, No ORM, 556 Lines of Hand-Built Dashboard

Django's admin panel is free and automatic. This storefront's buyer dashboard is three files, 556 lines, purpose-built instead of generated from a schema.

The ASoc Team8 min read

Django is a Python framework built around an ORM, a URL-and-view routing layer, and — unlike most full-stack frameworks — a free, auto-generated admin interface that ships enabled by default. Next.js has no ORM, no models.Model, and no admin scaffold; a Next.js team builds a back office by hand or doesn't have one. This storefront is the "doesn't have one" case, mostly: its buyer dashboard is three route files and 556 lines, purpose-built for one job, not generated from a schema.

The feature Django gives away for free

Every default Django project starts with django.contrib.admin in INSTALLED_APPS. Register a model — a handful of lines — and Django generates list views, detail forms, filtering, search, and bulk actions against it, no additional code. It's the single most-cited reason teams pick Django for anything with an internal-tooling component: the admin panel is not a paid add-on (unlike Laravel's comparable Nova) or a community package — it's part of the framework's own default startproject output.

This codebase has no equivalent, because Next.js doesn't ship one to have an equivalent to:

$ find src/app/dashboard -type f
src/app/dashboard/page.tsx
src/app/dashboard/settings/page.tsx
src/app/dashboard/layout.tsx
$ wc -l src/app/dashboard/*.tsx
  505 src/app/dashboard/page.tsx
    6 src/app/dashboard/settings/page.tsx
   45 src/app/dashboard/layout.tsx
  556 total

Three route files, 556 lines — and that's the route layer only. page.tsx composes purpose-built components (PurchaseRow, RefundButton, RedemptionPicker, YourProductsGrid, LicenseSummaryCard, DashboardTabs, AccountSettingsSection) from this project's existing atomic-design tree rather than a generated CRUD grid. Nothing about it is scaffolded from a schema — every field shown, every action button, was written for this exact buyer-facing job. A Django admin panel over the equivalent tables (orders, entitlement_slots, download_events) would appear with essentially no code, but it would be an internal admin tool built for staff, not a buyer-facing dashboard with redemption flows and license summaries — the two aren't actually interchangeable outputs, even though both start from "look at some rows tied to a user."

Where each framework's ORM would live

Django's models.Model classes double as the schema definition and the query interface — Order.objects.filter(user=request.user) reads naturally because the model, the migration, and the query all come from one class. This codebase keeps those three things separate and un-abstracted:

$ wc -l supabase/migrations/*.sql | tail -1
271 total
$ grep -rn '\.rpc(\|\.from("' src/ --include=*.ts | wc -l
17

271 lines of plain SQL define the schema — no Python class mirrors it. Seventeen call sites across eight different files (redemption.ts, refund.ts, checkout.ts, account.ts, entitlementsView.ts, the webhook and download routes, and one email helper) each call .from("entitlement_slots") or .rpc("record_download_within_limit", ...) directly, typed by hand against an interface written for that one call site — not a shared model class every query routes through. Django's QuerySet API — chainable, lazy, framework-aware, one class per table — has no analogue here at all; a Next.js team either adopts a schema-first ORM as a separate dependency (Prisma, Drizzle — this project uses neither) or writes typed rows by hand seventeen times over, which is what this codebase does.

Auth: middleware vs. explicit calls

Django's AuthenticationMiddleware attaches request.user to every view automatically, backed by session cookies Django itself manages. This app's session handling runs through src/proxy.ts, refreshing the Supabase session on every request — but nothing attaches a global "current user" the way Django's middleware does. Each Server Action or Route Handler that needs the caller re-derives it explicitly:

// src/lib/actions/redemption.ts — every write is scoped to the CALLER'S OWN session,
// never a client-supplied id (R-5)
async function getSession() {
  const { data: { user } } = await supabase.auth.getUser();
  ...
}

Django's version of this exists too (request.user.is_authenticated), but it's implicit — available on every request object without a call. This codebase's explicit-call shape is a direct consequence of Server Actions having no shared request object to attach a user to in the first place: React Server Components and Server Actions are function calls, not requests routed through a middleware stack with a mutable request object threaded through.

What each side actually buys a team

DjangoThis codebase
Admin/back-office UIdjango.contrib.admin, free, enabled by defaultHand-built (src/app/dashboard/, 556 lines), purpose-built for buyers, not staff
ORMmodels.Model + QuerySet — schema, migration and query in one classNone — plain SQL schema, hand-typed queries per call site
Request-scoped userrequest.user, attached by middleware automaticallyRe-derived explicitly per Server Action/Route Handler via supabase.auth.getUser()
REST API layerDjango REST Framework (separate package, but the ecosystem standard)Two Route Handlers (src/app/api/*/route.ts) for the two cases a Server Action can't cover
AuthorizationView-level @login_required/permission checks, app-layer onlyPostgres row-level security — runs inside the database regardless of the app layer, see this schema's RLS posture
Template renderingDjango templates, server-rendered HTML with {% %} tagsReact Server Components, no separate template language

Mistakes going either direction

SymptomCauseFix
Expecting request.user to just exist in a Server ActionNext.js has no global request object with middleware-attached stateCall supabase.auth.getUser() (or your provider's equivalent) explicitly at the top of the action
Looking for python manage.py makemigrationsThis repo's migrations are hand-written SQL, not generated from model diffsWrite the alter table statement yourself; there's no model class to diff against
Assuming RLS behaves like a Django permission checkA Django permission only runs when a view calls it; RLS runs inside Postgres for every matching query, app code or notTreat RLS as the backstop that holds even if a Server Action has a bug — see the zero-INSERT-policies posture in this repo's RLS post
Wanting a generated admin grid for internal opsNext.js has no django.contrib.admin equivalentEither build one by hand (this project's own dashboard is the model for "purpose-built, not generated") or reach for a hosted table-admin tool that talks to Postgres directly
Treating a Route Handler like a Django View classRoute Handlers are plain functions exporting GET/POST; there's no class-based dispatch, middleware chain or dispatch() overridePut per-method logic in the exported function itself, and compose shared behavior as plain function calls, not inheritance

Frequently asked questions

Would django.contrib.admin have replaced this storefront's dashboard outright? No — the two solve different jobs even though both display rows scoped to a user. Django's admin is an internal staff tool generated from models; this dashboard is a buyer-facing product surface with redemption flows, license summaries and refund buttons that a generated CRUD grid doesn't produce. A Django-backed version of this storefront would still need hand-built buyer-facing views — the admin panel would only cover the internal "look up an order" case, which this repo doesn't expose as a UI at all today.

Is Postgres row-level security comparable to Django's permission framework? Only partly. Django's @login_required/has_perm() checks are application code — skip the check, skip the protection. RLS policies run inside Postgres itself, independent of whether any app code remembered to check anything. This schema leans on that distinction: zero INSERT policies exist anywhere, so a write outside the sanctioned server functions fails at the database layer, not at a call a future refactor could accidentally skip.

Does Next.js have anything like Django REST Framework? Not built in, and this codebase doesn't reach for a REST-framework equivalent either — it has two Route Handlers total (a signed webhook receiver and a gated download endpoint), everything else runs as Server Actions called directly from components. A Next.js app that needed a full public REST API would typically add a separate library or hand-roll the convention DRF gives you automatically (serializers, viewsets, pagination).

Which is the better choice for a template-buyer storefront specifically? Neither wins categorically — it's a question of what the team already knows and whether an admin-generated back office matters. Django's batteries (ORM, admin, auth) save real time on internal tooling and CRUD-shaped screens. This storefront's bet was the opposite: a buyer-facing product with security posture (RLS, SECURITY DEFINER RPCs) as the actual hard problem, where a generated admin panel wouldn't have been the thing worth automating anyway.

What does this codebase use instead of Django REST Framework's serializers? Nothing framework-provided — each of the seventeen .from()/.rpc() call sites above returns whatever shape the query produces, and the calling code narrows it to a hand-written TypeScript interface (SlotRow, OrderRow) at the point of use. DRF's serializer classes centralize that shape-and-validation logic once per model and reuse it across every view; this codebase repeats the narrowing per call site instead, which is more code and fewer places a shape change can silently drift unnoticed, but also means no single "the serializer" file to keep in sync with the schema as it grows.

Templates in this post

ASoc Cover (an insurance company website), ASoc Echo (an AI chatbot landing page) and ASoc Edge (an applied-AI agency landing page) are all Next.js editions built the hand-rolled way this post describes — no ORM, no generated admin, Server Actions doing the write work.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the PHP-framework version of this same comparison, see Laravel vs. Next.js; for the database side of this stack, Supabase vs Firebase for a Template-Based SaaS.

Keep reading

Comparison9 min read

Next.js vs. Nuxt: 24 of 92 Components Opt Into the Client

Not React versus Vue — the real split is where each framework lets you draw the server/client boundary. Next.js draws it through the import graph; Nuxt's default is universal.

Read more
Comparison11 min read

Next.js vs React + Vite for Admin Dashboards: How to Choose

Both ship excellent dashboards. The decision comes down to where your data lives, whether you need SEO, and who deploys it — not to raw performance.

Read more
Comparison9 min read

Next.js vs SvelteKit: Where a Write Is Allowed to Live

Bundle size is the wrong axis for a meta-framework choice. The real divide is routing defaults and whether a mutation can live anywhere or only on the route that owns it.

Read more