Coming from Flask-Admin
starlette-admin began as a port of Flask-Admin's concepts to the ASGI ecosystem, so the migration is direct. You still subclass a ModelView, configure it with class attributes, and register it on an Admin instance. Most of the work is renaming attributes and moving from Flask's implicit request context to Starlette's explicit request object.
This guide maps the Flask-Admin API, attribute by attribute, to its starlette-admin equivalent.
Mental model
| Flask-Admin concept | starlette-admin equivalent |
|---|---|
Admin(app, name="...") |
Admin(engine, title="..."), then admin.mount_to(app) |
ModelView(Model, db.session) |
ModelView(Model); the Admin instance owns the engine and database sessions |
flask_admin.contrib.sqla |
starlette_admin.contrib.sqla |
flask_admin.contrib.mongoengine |
starlette_admin.contrib.mongoengine |
| peewee / pymongo backends | Beanie, Tortoise ORM, SQLModel, or a custom backend |
BaseView + @expose |
CustomView |
AdminIndexView |
Admin(index_view=...), DefaultIndexView |
Flask request context (flask.request) |
Explicit request: Request parameter on every hook |
| Sync methods | async methods; sync still works where callables are accepted |
Setup
There's no template_mode switch. The UI uses Tabler (Bootstrap 5) and includes dark mode. To change the look, write a custom BaseTheme or override the templates.
List page attributes
| Flask-Admin | starlette-admin | Notes |
|---|---|---|
column_list |
fields |
Also drives the detail and form pages. Use the exclude_fields_from_* attributes for per-page variations. |
column_exclude_list |
exclude_fields_from_list |
|
column_labels |
label= |
For example, StringField("title", label="Headline") |
column_descriptions |
help_text= |
Applies to the field definition. |
column_formatters |
formatter= on the field |
For example, StringField("title", formatter={RequestAction.LIST: lambda request, value: value[:40]}). |
column_formatters_detail / export formatters |
The same formatter= dict, keyed by RequestAction |
One mapping covers list, detail, and export formatting. Actions without an entry keep the raw value. |
column_type_formatters |
Per-field formatter=, or a custom field subclass |
There's no per-type registry. Attach the formatter to each field, or subclass the field and reuse it. |
Model properties or callables in column_list |
ComputedField or getter= on any field |
Adds virtual columns, or redirects an existing field's value lookup, without a subclass. |
| Custom WTForms fields (value coercion) | parser= on the field |
Replaces the field's default form or import parsing per RequestAction. |
column_searchable_list |
searchable_fields |
|
column_filters |
searchable_fields combined with per-field filters= |
Replaces the flat filter list with a visual builder that supports nested AND/OR groups. |
column_sortable_list |
sortable_fields |
|
column_default_sort |
fields_default_sort |
For example, [("created_at", True)] sorts in descending order. |
column_editable_list |
inline_editable_fields |
Users select a cell and edit it in place. |
page_size |
page_size |
|
can_set_page_size |
page_size_options |
Defaults to [10, 25, 50, 100]. Users pick from these options. |
column_display_pk |
Include the primary key in fields |
|
column_details_list |
fields minus exclude_fields_from_detail |
The detail page is built in. There's no can_view_details opt-in. |
Form attributes
| Flask-Admin | starlette-admin | Notes |
|---|---|---|
form_columns |
fields minus exclude_fields_from_create and exclude_fields_from_edit |
|
form_excluded_columns |
exclude_fields_from_create, exclude_fields_from_edit |
Separate visibility controls per form. |
form_overrides |
Explicit field instances in fields |
For example, fields = ["id", TextAreaField("bio")] |
form_args |
Constructor arguments on the field | For example, StringField("title", required=True, help_text="...") |
form_choices |
EnumField |
For example, EnumField("status", choices=[("draft", "Draft"), ("live", "Live")]) |
form_extra_fields |
Extra entries in fields |
Supports any field that isn't backed by a database column, such as a ComputedField. |
form_widget_args |
Field attributes | Set read_only, disabled, or placeholder directly on the field. |
form_rules |
form_layout |
Replaces flat rules with fieldsets, tabs, and responsive grids. |
create_modal / edit_modal |
Not available | Create and edit views render as full pages. |
on_form_prefill |
before_edit hook |
Export and import
CSV and JSON export is on by default. Row caps apply automatically, and spreadsheet formula escaping is an opt-in exporter setting. Import, which Flask-Admin doesn't provide, includes a preview step with per-row validation and optional updates to existing records by primary key. See Export and Import.
Actions
from starlette_admin import ActionSelection, action, flash
class PostView(ModelView):
actions = ["publish", "delete"]
@action(
name="publish",
text="Publish",
confirmation="Publish selected posts?",
)
async def publish(self, request: Request, selection: ActionSelection) -> None:
for post in await selection.rows():
post.published = True
flash(request, "Posts published")
The handler receives an ActionSelection object instead of raw IDs. It resolves rows lazily, exposes the active filters, and works the same way when a user selects all matching records across pages. Actions can also render a custom HTML form inside the confirmation dialog. For per-row operations, @row_action and @link_row_action replace custom column formatters.
Permissions and access control
Flask-Admin's can_* class flags become per-request methods in starlette-admin, so authorization decisions can depend on the signed-in user.
| Flask-Admin | starlette-admin | Notes |
|---|---|---|
is_accessible() |
is_accessible(request) |
Hides the view from the menu and blocks direct access. |
inaccessible_callback() |
Handled by the authentication flow | Unauthenticated requests redirect to the sign-in page. |
can_create = False |
def can_create(self, request): return False |
can_edit and can_delete follow the same pattern. |
can_view_details |
can_view_detail(request) |
The detail page exists by default. |
can_export |
can_export(request), plus can_import(request) |
|
| No equivalent | can_access_field(request, field) |
Controls field-level visibility per user. |
| No equivalent | is_action_allowed(request, name) |
Provides per-action authorization. |
With Flask-Admin, you integrate Flask-Login yourself. starlette-admin ships an AuthProvider with a ready-made sign-in page, and you implement the login, logout, and authenticate methods against your user store. An OAuthProvider covers OIDC redirect flows. The signed-in user is available everywhere as request.state.admin_user.
Model lifecycle hooks
| Flask-Admin | starlette-admin |
|---|---|
on_model_change(form, model, is_created) |
before_create(request, data, obj) / before_edit(request, data, obj) |
after_model_change |
after_create / after_edit |
on_model_delete |
before_delete |
after_model_delete |
after_delete |
get_query / get_count_query |
get_list_query / get_count_query, specific to the SQLAlchemy backend |
handle_view_exception |
Raise FormValidationError or ActionFailed |
Beyond per-view hooks, the event system lets a single handler observe every view. Flask-Admin has no equivalent.
from starlette_admin.events import AdminEvent, AfterCreateContext
async def audit(ctx: AfterCreateContext) -> None: ...
admin.events.on(AdminEvent.AFTER_CREATE, audit)
Custom views and the index page
| Flask-Admin | starlette-admin | Notes |
|---|---|---|
BaseView + @expose("/") |
CustomView(menu_label=..., path=..., widget=...) |
Compose pages from widgets without writing raw templates. |
| Custom template rendering | CustomView subclass |
Gives you full control over routes and responses. |
AdminIndexView |
Admin(index_view=...) |
Build dashboards from StatWidget, ChartWidget, TableWidget, and layout widgets. |
MenuLink |
Link view |
For example, admin.add_link(Link(menu_label="Docs", url="https://...")) |
| Categories in the menu | DropDown view |
Groups views together in the sidebar. |
FileAdmin |
Not available | File and image fields with local or S3 storage handle attachments. There's no server file browser. |
Inline models
An explicit class gives each inline model the full ModelView configuration surface: field selection, validation, and composite foreign key support. See Inline Forms.
Internationalization
Flask-Admin depends on Flask-Babel and the surrounding Flask environment. starlette-admin uses a configuration object instead:
from starlette_admin import I18nConfig
admin = Admin(engine, i18n_config=I18nConfig(default_locale="fr"))
Timezone-aware datetime rendering works the same way, through TimezoneConfig. See Internationalization and Timezones.
What you gain by switching
- An async stack. Runs natively on FastAPI and Starlette, with support for async SQLAlchemy, Beanie, and Tortoise ORM. Flask-Admin is synchronous.
- Security features built in. CSRF protection, upload filename sanitization, image content verification, and export row limits are on as soon as you instantiate
Admin, and you can enable spreadsheet formula escaping on the exporters. See Security. - Data import. A preview step validates every row before anything is written. Flask-Admin has no import feature.
- A dashboard widget system. Build index pages and custom views in Python instead of hand-writing templates.
- Modern design. An actively maintained codebase with a polished UI, built-in dark mode, and first-class type hints.
What you must adapt to
- Explicit request objects. There's no ambient request context. Every hook and permission method receives the
requestas a parameter. - Async handlers. Hooks and actions are coroutines, so keep blocking calls out of them or move that work to a thread.
- No
FileAdmin. If your workflow depends on browsing the server file system, starlette-admin doesn't cover it. - No create or edit modals. Forms render as full pages rather than popup modals.