Coming from Django Admin
If you are coming from Django Admin, you will feel at home in starlette-admin. Both frameworks generate administrative interfaces using declarative, per-model configurations. Both support inline editing, batch actions, and per-request permissions.
The differences are primarily structural. Instead of requiring Django, starlette-admin runs on any ASGI application, supports multiple ORMs, and allows you to plug in your own authentication system rather than imposing a built-in user model.
This guide maps every major ModelAdmin concept to its starlette-admin equivalent alongside direct code comparisons.
Mental Model
| Django Admin Concept | starlette-admin Equivalent |
|---|---|
AdminSite |
Admin instance mounted on your application |
ModelAdmin |
ModelView subclass |
admin.site.register(Model, ModelAdmin) |
admin.add_view(MyView(Model)) |
admin.site.urls in urlpatterns |
admin.mount_to(app) |
| Django ORM | SQLAlchemy, SQLModel, MongoEngine, Beanie, or Tortoise ORM via starlette_admin.contrib.* |
__str__ on the model |
__admin_repr__(self, request) (async and request-aware) |
| Form fields inferred from model fields | Fields inferred by the backend converter, customizable per field |
Registering a Model
from starlette_admin.contrib.sqla import Admin, ModelView
class PostView(ModelView):
fields = ["id", "title", "content", "published", "created_at"]
exclude_fields_from_list = ["content"]
searchable_fields = ["title", "content"]
admin = Admin(engine, title="Blog Admin", secret_key="change-me")
admin.add_view(PostView(Post, icon="fa fa-newspaper"))
admin.mount_to(app) # app is your FastAPI or Starlette instance
There are two key structural differences to notice here:
- Unified field declaration: The
fieldsattribute acts as the single source of truth for all pages. You then useexclude_fields_from_list,exclude_fields_from_detail,exclude_fields_from_create, andexclude_fields_from_editto define per-page variations. - Database lifecycle ownership: The
Admininstance owns the database engine, meaning views do not need a session explicitly passed to them.
List Page Options
| Django Admin | starlette-admin | Notes |
|---|---|---|
list_display |
fields minus exclude_fields_from_list |
A single field list drives every page. |
list_display with a callable or @admin.display |
ComputedField, or getter= on any field |
Example: ComputedField("full_name", getter=lambda request, obj: ...). Use getter= on a typed field (date, image, ...) to keep that type's rendering. |
| Reformatting a real column for display | formatter= on the field |
A dict[RequestAction, callable], so list, detail, and export can format differently. Django needs a callable plus admin_order_field to keep sorting; here the column stays sortable. |
search_fields |
searchable_fields |
Powers both full-text search and the filter builder. |
list_filter |
searchable_fields combined with per-field filters= |
Users get a visual builder with nested AND/OR groups instead of a fixed sidebar. See Filters. |
ordering |
fields_default_sort |
Example: fields_default_sort = [("created_at", True)] for descending order. |
admin_order_field / sortability |
sortable_fields |
Defaults to all fields being sortable. |
list_editable |
inline_editable_fields |
Allows clicking a cell to edit in place. |
list_per_page |
page_size, page_size_options |
Controls pagination limits. |
date_hierarchy |
Date filters (between, in the past, etc.) |
There is no dedicated drill-down bar; the filter builder covers this use case. |
empty_value_display |
A formatter= entry, or null_template |
Formatters receive None values, so they can substitute a placeholder; null_template swaps the rendered markup instead. |
Forms
| Django Admin | starlette-admin | Notes |
|---|---|---|
fields / exclude |
fields, exclude_fields_from_create, exclude_fields_from_edit |
Controls form field visibility. |
fieldsets |
form_layout |
Compose freely with FieldsetWidget, TabsWidget, GridWidget, and RowWidget. |
readonly_fields |
read_only=True on the field |
Alternatively, exclude the field from the create/edit views. |
prepopulated_fields |
SlugField("slug", populate_from="title") |
Provides the same live slugification behavior. |
autocomplete_fields, raw_id_fields |
Default behavior of HasOne / HasMany |
Relation widgets are Select2 inputs with server-side search out of the box. |
filter_horizontal / filter_vertical |
HasMany |
Rendered as a multi-select component with search capability. |
formfield_overrides |
Explicit entries in the fields list |
Replace the auto-detected field directly: fields = ["id", TextAreaField("bio")] |
| Custom form validation | Field validators= or FormValidationError in hooks |
See Validators. |
Form field to_python() / custom coercion |
parser= on the field |
Replaces the field's default form or import parsing per RequestAction. |
| Model form help text | help_text= |
Available on any field definition. |
Fieldsets Example
The form_layout attribute offers significantly more flexibility than traditional fieldsets. You can easily build out tabs, responsive grids, and nested layouts. For more details, see Form Layout.
Inlines
Foreign keys are auto-detected when unambiguous, and composite foreign keys are fully supported. See Inline Forms for advanced configurations.
Actions
from starlette_admin import ActionSelection, action, flash
class ArticleView(ModelView):
actions = ["make_published", "delete"]
@action(
name="make_published",
text="Mark selected articles as published",
confirmation="Publish the selected articles?",
)
async def make_published(
self, request: Request, selection: ActionSelection
) -> None:
for article in await selection.rows():
article.published = True
flash(request, "Articles published")
Unlike Django Admin, which passes a QuerySet, the starlette-admin handler receives an ActionSelection object. This object evaluates rows, primary keys, and active filters lazily. It also behaves consistently when a user chooses to "select all matching" records.
Furthermore, actions support custom HTML forms directly in the confirmation dialog, a feature that requires building an intermediate page in Django Admin. For per-row operations, starlette-admin provides @row_action and @link_row_action, which have no direct equivalent in Django Admin.
Permissions and Authentication
While Django Admin delegates heavily to django.contrib.auth, starlette-admin splits the problem into two distinct parts: an AuthProvider answers "who is this user," and per-view methods answer "what can they do."
| Django Admin | starlette-admin |
|---|---|
django.contrib.auth login |
AuthProvider (built-in login page) or OAuthProvider (OIDC redirect flow) |
request.user |
request.state.admin_user |
has_module_permission |
is_accessible(request) on the view |
has_view_permission |
can_view_detail(request) |
has_add_permission |
can_create(request) |
has_change_permission |
can_edit(request) |
has_delete_permission |
can_delete(request) |
get_readonly_fields per user |
can_access_field(request, field) |
| N/A | can_export(request), can_import(request), is_action_allowed(request, name) |
Example of restricting deletion based on user roles:
class ArticleView(ModelView):
def can_delete(self, request: Request) -> bool:
return "admin" in request.state.admin_user.roles
Because every can_* method receives the request object, authorization decisions can dynamically evaluate the current user, HTTP headers, or any other request-specific data.
Save Hooks and Signals
| Django Admin | starlette-admin | Notes |
|---|---|---|
save_model(request, obj, form, change) |
before_create / before_edit on the view |
Async-native; receives the parsed form data and the model instance. |
delete_model |
before_delete |
Handles pre-deletion logic. |
post_save and other signals |
Events | Example: admin.events.on(AdminEvent.AFTER_CREATE, handler) broadcasts to all views. |
LogEntry change history |
Build using the event system | Subscribe to AFTER_CREATE, AFTER_EDIT, and AFTER_DELETE to populate your own audit table. |
messages.success(request, ...) |
flash(request, ...) |
See Flash Messages. |
Site-Wide Configuration
| Django Admin | starlette-admin |
|---|---|
admin.site.site_header, site_title |
Admin(title="...") |
| Custom logo via template override | Admin(logo_url="...", login_logo_url="...", favicon_url="...") |
AdminSite.index_template |
Admin(index_view=...) utilizing widgets for a rich dashboard |
Template overrides in templates/admin/ |
Admin(templates_dir="..."), see Templates |
Multiple AdminSite instances |
Multiple Admin instances mounted at different application paths |
ModelAdmin.get_queryset |
get_list_query, get_count_query, or get_detail_query (for the SQLAlchemy backend) |
USE_I18N, LANGUAGES |
Admin(i18n_config=I18nConfig(default_locale="fr")) |
TIME_ZONE |
Admin(timezone_config=TimezoneConfig(...)), see i18n and Timezones |
What You Gain by Switching
- End-to-end async architecture: Handlers, lifecycle hooks, and widget callbacks can all be asynchronous coroutines running on your existing event loop, side-by-side with your FastAPI endpoints.
- Database flexibility: The exact same admin skills and configurations apply whether you use SQLAlchemy, SQLModel, MongoDB (via MongoEngine or Beanie), or Tortoise ORM.
- Native export and import capabilities: Built-in support for CSV, JSON, and PDF, plus extended support for Excel and other formats via
tablib. Export records instantly or safely import bulk data using a preview-first wizard that enforces strict row-level validation and supports optional primary key upserts. See Export and Import. - Integrated dashboard widgets: Stat cards, ApexCharts, and layout grids compose easily into index pages and custom views. You do not need to hunt for an external theme package to build complex dashboards. See Custom Views and Widgets.
- Modern user interface: Uses Tabler (Bootstrap 5) to provide a polished UI that includes dark mode, column visibility toggles, and search highlighting by default.
What You Must Bring Yourself
- Custom user authentication: There is no bundled user model or predefined permission database. You must implement
AuthProvider.authenticate()to read from whichever datastore your application already utilizes. - Audit logging: starlette-admin does not generate an automatic
LogEntrytable. You will need to wire the event system to your own custom audit table. - Model-level UI configurations: Django conveniences like model-level
choices,verbose_name, and validators do not automatically transfer. You will need to declare these explicitly on the starlette-admin field definition (e.g., usingEnumField,label=, andvalidators=).