Coming from Django
Same needs, new homes. This page shows the mapping three ways: the table, the request flow, and the code.
The translation table
| Django | Here | Where |
|---|---|---|
| ORM (models) | Drizzle ORM | Database |
makemigrations / migrate | drizzle-kit generate / migrate | packages/db/drizzle/ |
| Views / DRF / Ninja | Server functions | Server functions |
| Forms / serializers | Zod (inputValidator) | inside each server function |
django.contrib.auth | Better Auth | Auth |
login_required | requireSession() | @repo/auth/session |
is_staff / is_superuser | isSiteAdmin + requireSiteAdmin() | example/src/lib/access.ts |
| guardian / rules (object permissions) | typescript-rules + capabilities | example/src/lib/permissions/ |
| The admin (changelist, actions) | @repo/admin-ui + /admin | Admin UI |
| django-import-export (preview → confirm) | @repo/import-export | Import/export |
| Celery + beat | Hatchet | Background jobs |
send_mail | sendEmail() | |
| django-storages | putObject / getObject | Storage |
| constance (runtime settings) | getConfig / setConfig | Runtime config |
| Signals → audit trail | explicit recordAudit() | Audit |
| Templates (the T in MTV) | React with SSR | example/src/routes/ |
gettext / django.po i18n | Paraglide JS — typed message functions | example/messages/sv.json + en.json |
settings.py | .env + validated env | Environment |
Three things disappear rather than map:
- CORS — frontend and backend are the same origin, the same server.
- CSRF token plumbing — SameSite cookies + JSON payloads.
- DRF routers — file-based routing plus typed function calls.
The request flow
How Django serves our classic React frontend:
The same operation here:
That last note is the biggest day-to-day quality difference: one contract, one place. In the Django setup we maintain the serializer and the TS types separately; here the function signature is the contract.
The code: validation and types in one
In Django you write a serializer and hope the frontend types match. Here the Zod
schema defines both the validation and the type — hover Input:
import { } from "zod";
const = .({
: .(),
: .().(1).(120),
: .().(60).(),
});
type = .<typeof >;
And mistakes surface at compile time, not in production — count is not in the
schema:
import { } from "zod";
const = .({ : .() });
.({ : "K-01" });
type = .<typeof >;
const : = { : "K-01", : 3 };The model: same idea, new syntax
Django's models.Model becomes a pgTable. This is the real user table from the
Toolkit's foundation schema — included live from the source, not a copy:
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").notNull().default(false),
image: text("image"),
isSiteAdmin: boolean("is_site_admin").notNull().default(false),
deactivatedAt: timestamp("deactivated_at"),
// Free-form per-user key/value store. Used by `user_info` tasks to persist
// collected field values (keyed by `task:<id>`); other future profile data
// can live here too.
metadata: jsonb("metadata"),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});Things to notice:
- No class magic. The table is a value; the row type is derived
(
typeof user.$inferSelect). - Migrations are generated from code, like
makemigrations, but the diff is plain SQL inpackages/db/drizzle/— readable in code review. is_activeisdeactivatedAt— when set, the user is locked out on the next request (see Auth).