Migration Guide
This page collects the upgrade instructions between starlette-admin releases. Jump to the section matching the version you are upgrading from.
From 0.17.x to 1.0.0
This release refactors the internals of starlette-admin and introduces a large set of new features. While the high-level API remains largely unchanged, the most significant update is the rewrite of the list page rendering. We have dropped DataTables in favor of a server-rendered table. Most other updates consist of renames or minor signature changes.
This guide covers every breaking change in the order you are most likely to encounter them. Each section compares the old API alongside its replacement. If your implementation relies on the basics or light customizations (such as an Admin instance, a few ModelView subclasses, fields, and searchable_fields), your migration will likely be limited to the Requirements and Admin Constructor sections, plus a few renames.
Customizations of the old list page require the most attention. DataTables options and JavaScript render functions have no direct equivalent and must be ported to server-side templates (see DataTables Removal).
Tip
Upgrade your dependencies in one step and start your application. Most removed or renamed attributes raise clear errors at startup rather than failing silently at runtime.
What's New
Beyond the breaking changes outlined below, this release includes:
- Native list tables: DataTables has been removed in favor of a built-in, server-rendered implementation. Table state is now entirely URL-driven, meaning all page, filter, and sort configurations are immediately shareable and bookmarkable.
- Filters: A nested
AND/ORfilter builder replaces the DataTables SearchBuilder. Filters are derived from field types and are fully extensible in pure Python. You can write a filter class without needing any JavaScript. - Import and Server-Side Export: Import data from CSV, JSON, Excel, and more with per-row error reporting, alongside server-side exporters (CSV, JSON, Excel, PDF, etc.) that replace client-side DataTables buttons.
- Events: Subscribe to lifecycle hooks like
before_create,after_edit_committed,after_login, and various action events. - Themes and Plugins: Package and reuse custom aesthetics and behaviors. Cookiecutter templates are available to help you get started quickly.
- Widgets and Dashboards: Build index pages and custom views using
StatWidget,ChartWidget,TableWidget, and more. - Form Layout: Arrange create/edit forms logically using rows, columns, fieldsets, and tabs.
- Inline Edit: Edit a single field directly from the list page.
- Inline Forms: Edit related models inside a parent form using
InlineModelView. - Other Enhancements: Flash messages, OAuth login, a Tortoise ORM backend, new fields (
ComputedField,SlugField,UUIDField,IPAddressField), field-levelvalidators, and clipboard copy functionality on any field. - Logging: The package now logs internally under the
starlette_adminnamespace, silent by default. PassAdmin(debug=True)or callstarlette_admin.logging.configure_logging()to see request routing, middleware, and permission decisions in the console, which is especially handy while migrating. - Expanded Test Coverage: The test suite is now substantially larger, featuring Playwright end-to-end tests that validate critical workflows across the admin interface.
Requirements
- Python Support: Python 3.11 or newer is required. Support for Python 3.9 and 3.10 has been dropped.
- Core Dependencies:
itsdangerousis now a core dependency used to sign admin cookies (CSRF tokens and flash messages). -
New Optional Extras:
Extra Enables starlette-admin[email]EmailFieldserver-side validation viaemail-validatorstarlette-admin[pdf]PDF export via reportlabstarlette-admin[s3]S3 file storage via aiobotocorestarlette-admin[tinymce]TinyMCEEditorFieldHTML sanitization vianh3starlette-admin[i18n]Translations via babel(unchanged) -
Beanie Backend: Beanie 2.0+ is required.
- Odmantic Backend: Removed. If you depend on it, stay on
starlette-admin<=0.17.1and show your interest by opening an issue; support can be re-added if there is enough demand.
The Admin Constructor
# Before
admin = Admin(engine, statics_dir="statics")
# After
admin = Admin(engine, static_dir="statics", secret_key=os.environ["ADMIN_SECRET_KEY"])
statics_diris renamed tostatic_dir.- Set a
secret_key. This key signs the CSRF and flash-message cookies. If omitted, a random key generates at startup (which is fine for development). However, signed values will be invalidated on every restart and across multiple workers. Always pass a stable secret in production. logo_url,login_logo_url, andfavicon_urlnow accept a callable(request) -> str | None. This replaces the per-request branding previously provided byAdminConfig.- New optional parameters:
theme,plugins,additional_loaders,import_config, andexport_config. - SQLAlchemy specifics: The first argument is now
session_provider. It accepts anEngineorAsyncEngine, and now also accepts asessionmakerorasync_sessionmaker. ExistingAdmin(engine)calls will continue to work. timezone_configdefaults toTimezoneConfig()instead ofNone. Datetimes now display in the viewer's local timezone by default. Passtimezone_config=Noneto retain raw values.
Renamed View Identifiers
The naming convention for views is now unified. Update your ModelView constructors and class attributes accordingly:
| Before | After |
|---|---|
identity |
key |
name |
display_name |
label |
menu_label |
form_include_pk |
show_pk_in_forms |
# Before
admin.add_view(PostView(Post, identity="post", name="Post", label="Posts"))
# After
admin.add_view(PostView(Post, key="post", display_name="Post", menu_label="Posts"))
Note that Link and DropDown also use menu_label instead of label.
DataTables Removal
The list page no longer uses DataTables. The attributes that previously configured it have been removed entirely:
| Removed | Replacement |
|---|---|
datatables_options |
None. The table is server-rendered. Customize via templates. |
search_builder |
The new filter builder, enabled by searchable_fields. |
responsive_table |
None. The table handles overflow natively. |
save_state |
Always on. List state (page, sort, filters, search, visible columns) now lives in the URL. |
BaseField.search_builder_type |
BaseField.filters (a list of filter classes). |
BaseField.render_function_key |
BaseField.list_template (server-side Jinja template). |
If you previously wrote custom JavaScript render functions or DataTables plugins, port them to list_template overrides. Each field now renders its list cell directly from templates/fields/list/*.html.
Actions
Batch action handlers now receive an ActionSelection object instead of a list of primary keys. This supports the new "select all matching" banner, targeting every row that matches the current filter without materializing them on the client side.
# Before
@action(name="publish", text="Publish")
async def publish_action(self, request: Request, pks: List[Any]) -> str:
for article in await self.find_by_pks(request, pks):
...
return f"{len(pks)} articles were published"
# After
@action(name="publish", text="Publish")
async def publish_action(self, request: Request, selection: ActionSelection) -> None:
for article in await selection.rows():
...
flash(request, f"{await selection.count()} articles were published")
- Methods like
selection.rows(),selection.pks(), andselection.count()resolve target rows lazily. This applies whether the user checked rows individually or selected all matching rows. - Properties like
selection.is_select_all,selection.filters, andselection.qallow you to push the operation down as a single bulk query. - Returning a success message string is replaced by flash messages.
- Row action handlers retain their original
(request, pk)signature. - New
@actionoptions:header,allow_empty_selection,dedicated_button,modal_size, and per-requestformcallables.
Authentication
The starlette_admin/auth.py module is now the starlette_admin.auth package. Existing imports from starlette_admin.auth will continue to work, but the provider contract has changed.
# Before
class MyAuthProvider(AuthProvider):
async def login(self, username, password, remember_me, request, response):
request.session.update({"username": username})
return response
async def logout(self, request, response):
request.session.clear()
return response
async def is_authenticated(self, request) -> bool:
request.state.user = my_users_db.get(request.session.get("username"))
return request.state.user is not None
def get_admin_user(self, request) -> AdminUser:
return AdminUser(username=request.state.user["name"])
def get_admin_config(self, request) -> AdminConfig:
return AdminConfig(app_title="My Admin")
# After
class MyAuthProvider(AuthProvider):
async def login(self, username, password, remember_me, request):
if username in my_users_db:
request.session.update({"username": username})
return None # default redirect (`next` param or admin index)
raise LoginFailed("Invalid username or password")
async def logout(self, request):
request.session.clear()
async def authenticate(self, request) -> AdminUser | None:
user = my_users_db.get(request.session.get("username"))
return AdminUser(username=user["name"]) if user else None
- The methods
is_authenticated,get_admin_user, andget_admin_configare merged into a singleauthenticate(request) -> AdminUser | Nonemethod. ReturningNoneindicates an unauthenticated state. - The
loginandlogoutmethods no longer receive or return the preparedresponse. ReturnNonefor the default redirect, or return a customResponseto override this behavior. AdminConfigis removed. Handle per-request titles and logos using the callable form oflogo_urlandlogin_logo_urlon theAdmininstance.- A built-in
OAuthProviderhandles OAuth2/OIDC login flows out of the box. - The
login_not_requireddecorator remains unchanged.
Export and Import
Exports have moved from client-side DataTables buttons to server-side streaming endpoints. Import functionality is entirely new, and ExportType no longer exists.
# Before
from starlette_admin import ExportType
class PostView(ModelView):
export_types = [ExportType.CSV, ExportType.EXCEL]
export_fields = ["id", "title"]
# After
class PostView(ModelView):
exporters = ["csv", "xlsx"]
importers = ["csv", "json"]
exclude_fields_from_export = ["content"]
exclude_fields_from_import = ["id"]
export_typesis nowexporters. This accepts a list of format names orBaseExporterinstances. Supported built-ins includecsv,json,tsv,xlsx,ods,html,yaml, andpdf. Formats other thancsvorjsonrequiretablib, and PDF requires thepdfextra.importersaccepts a list of format names orBaseImporterinstances. Supported built-ins includecsv,tsv,json,yaml,xlsx,xls,ods,dbf, andhtml. Formats other thancsv,tsv, orjsonrequiretablib.export_fields(an include list) is replaced byexclude_fields_from_export(an exclude list). This matches the naming convention of otherexclude_fields_from_*attributes.- Fields also accept
exclude_from_exportandexclude_from_importon an individual basis. - Configure global limits using
ExportConfigandImportConfigon theAdmininstance. Refer to the Export & Import documentation for details.
Custom Fields and Template Overrides
Field templates are now reorganized. Update your paths if you override built-in templates or ship custom fields:
| Before | After |
|---|---|
templates/displays/*.html |
templates/fields/detail/*.html |
templates/forms/*.html |
templates/fields/form/*.html |
| (client-side render function) | templates/fields/list/*.html |
BaseField.display_template |
BaseField.detail_template |
BaseField.form_template (path) |
Same attribute name, new path prefix fields/form/ |
# Before
@dataclass
class RatingField(BaseField):
display_template: str = "displays/rating.html"
form_template: str = "forms/rating.html"
render_function_key: str = "rating"
# After
@dataclass
class RatingField(BaseField):
detail_template: str = "fields/detail/rating.html"
form_template: str = "fields/form/rating.html"
list_template: str = "fields/list/rating.html"
New per-field capabilities to explore include validators, filters, default, hooks (getter, formatter, parser), copy_to_clipboard, and an extra dictionary for arbitrary metadata. Review the Custom Fields documentation for more information.
CustomView
The CustomView class no longer accepts template_path and methods. Build simple pages using widgets. For pages requiring full control, subclass CustomView and declare your routes directly.
# Before
admin.add_view(CustomView(label="Home", path="/home", template_path="home.html"))
# After: widget-based page
admin.add_view(
CustomView(
menu_label="System Status",
path="/status",
widget=StatWidget(title="Pending jobs", value_callback=count_pending_jobs),
)
)
# After: full control
class HomeView(CustomView):
menu_label = "Home"
path = "/home"
@route("")
async def index(self, request: Request) -> Response:
return self.templates.TemplateResponse(request=request, name="home.html")
The @route decorator also allows any view to expose extra endpoints for requirements like JSON chart data or webhooks.
Custom Backends
If you implemented BaseModelView against a custom datasource, note the updated data-access contract:
# Before
async def find_all(self, request, skip=0, limit=100, where=None, order_by=None): ...
async def count(self, request, where=None): ...
# After
async def find_all(
self, request, skip=0, limit=100, q=None, sorts=None, filters=None
): ...
async def count(self, request, q=None, filters=None): ...
- The string-typed
whereparameter is split intoq(for full-text search terms) andfilters(a typedFilterGrouptree provided by the filter builder). - The
order_byparameter (previously a list of"field direction"strings) is nowsorts, taking a list of(field_name, direction)tuples. - Each backend now ships with a filter registry mapping field types to filter implementations. Check the Custom Backend documentation for the full contract and a working example.
Behavior Changes to Review
- Timezones: Datetimes render in the viewer's local timezone by default (see the
timezone_confignote in the Admin Constructor section). - URL State: List state now lives in the URL. Bookmarked admin URLs from previous versions will land on default list states, because saved DataTables states are not migrated.
- Email Validation:
EmailFieldnow validates on the server whenemail-validatoris installed. - CSRF Protection: CSRF protection is built-in and cookie-based. If you previously wrapped the admin with custom CSRF middleware, you can safely remove it. Ensure your
secret_keyis set so tokens survive server restarts. - FileField Upload Size:
FileField.max_sizenow defaults to 50 MB instead of unlimited. Passmax_size=Noneto restore the old unbounded behavior, or set an explicit value to change the cap.
Removed with No Replacement
AdminConfig(see Authentication).datatables_options,responsive_table, andsave_state(see DataTables Removal).- The Odmantic backend (see Requirements).
Getting Help
If you encounter a migration issue not covered in this guide, please open an issue. Include a minimal reproduction of the problem and specify the version you are upgrading from. Running with Admin(debug=True) often reveals the cause directly, and the resulting logs make a great addition to your report.