Migrate starlette-admin 0.17.x to 1.0.0
Apply every breaking change from the official migration guide, published at https://jowilf.github.io/starlette-admin/migration/. For feature work after the upgrade, use the starlette-admin skill.
Workflow
- Locate all code using starlette-admin:
grep -rl "starlette_admin" --include="*.py". Also search template directories for overridden admin templates.
- Run the detection greps below to build the list of impacted call sites before editing anything.
- Apply the transformations section by section. Mechanical renames first, then behavioral changes.
- Flag anything under "Needs human decision" to the user instead of guessing.
- Start the app once after migrating: removed or renamed attributes raise clear errors at startup rather than failing silently at runtime.
Step 0: Requirements
- Python 3.11+ is required. Check
pyproject.toml / setup.cfg and CI matrices for 3.9 or 3.10.
- Beanie backend requires Beanie 2.0+.
- Odmantic backend is removed. If the code imports
starlette_admin.contrib.odmantic, stop and tell the user: stay on starlette-admin<=0.17.1 or migrate models to Beanie.
- New extras to add when the feature is used:
email (EmailField server-side validation), pdf (PDF export), s3 (S3 file storage), tinymce (HTML sanitization).
Step 1: Admin constructor
Detect: grep -rn "statics_dir\|Admin(" --include="*.py"
statics_dir becomes static_dir.
- Add
secret_key=... sourced from the environment. It signs CSRF and flash cookies. Without it, signed values invalidate on restart and break across workers.
timezone_config now defaults to TimezoneConfig(). Datetimes render in the viewer's local timezone. Pass timezone_config=None only if the user needs the old raw display.
- Per-request branding moved from
AdminConfig to callable logo_url, login_logo_url, favicon_url parameters: (request) -> str | None.
- SQLAlchemy: the first argument is now
session_provider and also accepts a sessionmaker or async_sessionmaker. Existing Admin(engine) calls keep working.
Step 2: View identifier renames
Detect: grep -rn "identity=\|label=\|form_include_pk\|identity:\|label:" --include="*.py"
Apply in ModelView, Link, DropDown, and CustomView (constructor args and class attributes):
| Before |
After |
identity |
key |
name |
display_name |
label |
menu_label |
form_include_pk |
show_pk_in_forms |
Careful with name and label: rename them only on view classes, not on fields or actions (@action(name=..., text=...) and BaseField.label are unchanged).
Step 3: DataTables removal
Detect: grep -rn "datatables_options\|search_builder\|responsive_table\|save_state\|render_function_key" --include="*.py"
| Removed |
Replacement |
datatables_options |
Delete. Table is server-rendered; customize via templates. |
search_builder |
Delete. Filter builder is enabled by searchable_fields. |
responsive_table |
Delete. |
save_state |
Delete. List state lives in the URL. |
BaseField.search_builder_type |
BaseField.filters (list of filter classes). |
BaseField.render_function_key |
BaseField.list_template (Jinja template under fields/list/). |
Custom JavaScript render functions have no direct equivalent. Port their logic to a server-side Jinja template and set list_template on the field. Flag each one to the user with the original JS so they can validate the ported template.
Step 4: Batch actions
Detect: grep -rn "@action" --include="*.py"
Batch handlers change from (self, request, pks: List[Any]) -> str to (self, request, selection: ActionSelection) -> None:
- Replace
await self.find_by_pks(request, pks) with await selection.rows().
- Replace
len(pks) with await selection.count(); selection.pks() returns the keys.
- Replace the returned message string with
flash(request, message).
- For bulk pushdown,
selection.is_select_all, selection.filters, and selection.q describe the selection without materializing rows.
- Row action handlers (
@row_action) keep the (request, pk) signature. Do not touch them.
Step 5: Auth providers
Detect: grep -rn "AuthProvider\|is_authenticated\|get_admin_user\|get_admin_config\|AdminConfig" --include="*.py"
- Merge
is_authenticated, get_admin_user, and get_admin_config into one method: async def authenticate(self, request) -> AdminUser | None. Return None when unauthenticated.
login and logout no longer receive or return response. Return None for the default redirect or a custom Response to override. Raise LoginFailed on bad credentials.
AdminConfig is removed. Move per-request titles and logos to callable logo_url / login_logo_url on Admin.
login_not_required is unchanged. A built-in OAuthProvider is available for OAuth2/OIDC flows.
Step 6: Export and import
Detect: grep -rn "ExportType\|export_types\|export_fields" --include="*.py"
export_types = [ExportType.CSV, ...] becomes exporters = ["csv", ...]. Built-ins: csv, json, tsv, xlsx, ods, html, yaml, pdf. Remove the ExportType import.
export_fields (include list) becomes exclude_fields_from_export (exclude list). Invert the field list against the view's fields.
- New capabilities to mention, not auto-add:
importers, exclude_fields_from_import, per-field exclude_from_export / exclude_from_import, ExportConfig / ImportConfig on Admin.
Step 7: Custom fields and template overrides
Detect: grep -rn "display_template\|form_template\|displays/\|forms/" --include="*.py" and look for template dirs containing displays/ or forms/.
| 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 |
form_template path prefix |
fields/form/ |
Move the template files and update the attribute paths together.
Step 8: CustomView
Detect: grep -rn "CustomView" --include="*.py"
template_path and methods are removed. Two replacement patterns:
- Simple page: pass a
widget= (for example StatWidget, ChartWidget, TableWidget).
- Full control: subclass
CustomView, set menu_label and path, and declare endpoints with @route. Render with self.templates.TemplateResponse(request=request, name=...).
Pick based on what the old template did; when unclear, subclass with @route since it can render the existing template unchanged.
Step 9: Custom backends
Detect: grep -rn "BaseModelView" --include="*.py" (subclasses implementing find_all / count).
where splits into q (full-text search string) and filters (typed FilterGroup tree).
order_by (list of "field direction" strings) becomes sorts (list of (field_name, direction) tuples).
- Signatures:
find_all(self, request, skip=0, limit=100, q=None, sorts=None, filters=None) and count(self, request, q=None, filters=None).
Behavior changes to report
Include these in the final summary even when no code change is needed:
- Datetimes now display in the viewer's local timezone by default.
- Old bookmarked list URLs fall back to default list state; DataTables saved states are not migrated.
EmailField validates server-side when email-validator is installed.
- CSRF protection is built-in and cookie-based. Remove any custom CSRF middleware wrapping the admin, and confirm
secret_key is set.
Needs human decision
Ask the user instead of guessing when you find:
- JavaScript render functions or DataTables plugins (Step 3): the ported template needs their review.
datatables_options carrying meaningful config (custom ordering, page lengths): confirm the server-side equivalent they want.
- Odmantic usage: version pin versus Beanie migration.
- Missing
secret_key source: which env var or settings entry to use.
1---2name: starlette-admin-migration3description: Migrate an application from starlette-admin 0.17.x to 1.0.0. Use when upgrading starlette-admin, fixing breaking changes after a version bump, or when errors mention removed attributes such as statics_dir, identity, datatables_options, ExportType, search_builder, display_template, is_authenticated, or AdminConfig.4---56# Migrate starlette-admin 0.17.x to 1.0.078Apply every breaking change from the official migration guide, published at https://jowilf.github.io/starlette-admin/migration/. For feature work after the upgrade, use the `starlette-admin` skill.910## Workflow11121. Locate all code using starlette-admin: `grep -rl "starlette_admin" --include="*.py"`. Also search template directories for overridden admin templates.132. Run the detection greps below to build the list of impacted call sites before editing anything.143. Apply the transformations section by section. Mechanical renames first, then behavioral changes.154. Flag anything under "Needs human decision" to the user instead of guessing.165. Start the app once after migrating: removed or renamed attributes raise clear errors at startup rather than failing silently at runtime.1718## Step 0: Requirements1920* Python 3.11+ is required. Check `pyproject.toml` / `setup.cfg` and CI matrices for 3.9 or 3.10.21* Beanie backend requires Beanie 2.0+.22* Odmantic backend is removed. If the code imports `starlette_admin.contrib.odmantic`, stop and tell the user: stay on `starlette-admin<=0.17.1` or migrate models to Beanie.23* New extras to add when the feature is used: `email` (EmailField server-side validation), `pdf` (PDF export), `s3` (S3 file storage), `tinymce` (HTML sanitization).2425## Step 1: Admin constructor2627Detect: `grep -rn "statics_dir\|Admin(" --include="*.py"`2829* `statics_dir` becomes `static_dir`.30* Add `secret_key=...` sourced from the environment. It signs CSRF and flash cookies. Without it, signed values invalidate on restart and break across workers.31* `timezone_config` now defaults to `TimezoneConfig()`. Datetimes render in the viewer's local timezone. Pass `timezone_config=None` only if the user needs the old raw display.32* Per-request branding moved from `AdminConfig` to callable `logo_url`, `login_logo_url`, `favicon_url` parameters: `(request) -> str | None`.33* SQLAlchemy: the first argument is now `session_provider` and also accepts a `sessionmaker` or `async_sessionmaker`. Existing `Admin(engine)` calls keep working.3435## Step 2: View identifier renames3637Detect: `grep -rn "identity=\|label=\|form_include_pk\|identity:\|label:" --include="*.py"`3839Apply in `ModelView`, `Link`, `DropDown`, and `CustomView` (constructor args and class attributes):4041| Before | After |42| --- | --- |43| `identity` | `key` |44| `name` | `display_name` |45| `label` | `menu_label` |46| `form_include_pk` | `show_pk_in_forms` |4748Careful with `name` and `label`: rename them only on view classes, not on fields or actions (`@action(name=..., text=...)` and `BaseField.label` are unchanged).4950## Step 3: DataTables removal5152Detect: `grep -rn "datatables_options\|search_builder\|responsive_table\|save_state\|render_function_key" --include="*.py"`5354| Removed | Replacement |55| --- | --- |56| `datatables_options` | Delete. Table is server-rendered; customize via templates. |57| `search_builder` | Delete. Filter builder is enabled by `searchable_fields`. |58| `responsive_table` | Delete. |59| `save_state` | Delete. List state lives in the URL. |60| `BaseField.search_builder_type` | `BaseField.filters` (list of filter classes). |61| `BaseField.render_function_key` | `BaseField.list_template` (Jinja template under `fields/list/`). |6263Custom JavaScript render functions have no direct equivalent. Port their logic to a server-side Jinja template and set `list_template` on the field. Flag each one to the user with the original JS so they can validate the ported template.6465## Step 4: Batch actions6667Detect: `grep -rn "@action" --include="*.py"`6869Batch handlers change from `(self, request, pks: List[Any]) -> str` to `(self, request, selection: ActionSelection) -> None`:7071* Replace `await self.find_by_pks(request, pks)` with `await selection.rows()`.72* Replace `len(pks)` with `await selection.count()`; `selection.pks()` returns the keys.73* Replace the returned message string with `flash(request, message)`.74* For bulk pushdown, `selection.is_select_all`, `selection.filters`, and `selection.q` describe the selection without materializing rows.75* Row action handlers (`@row_action`) keep the `(request, pk)` signature. Do not touch them.7677## Step 5: Auth providers7879Detect: `grep -rn "AuthProvider\|is_authenticated\|get_admin_user\|get_admin_config\|AdminConfig" --include="*.py"`8081* Merge `is_authenticated`, `get_admin_user`, and `get_admin_config` into one method: `async def authenticate(self, request) -> AdminUser | None`. Return `None` when unauthenticated.82* `login` and `logout` no longer receive or return `response`. Return `None` for the default redirect or a custom `Response` to override. Raise `LoginFailed` on bad credentials.83* `AdminConfig` is removed. Move per-request titles and logos to callable `logo_url` / `login_logo_url` on `Admin`.84* `login_not_required` is unchanged. A built-in `OAuthProvider` is available for OAuth2/OIDC flows.8586## Step 6: Export and import8788Detect: `grep -rn "ExportType\|export_types\|export_fields" --include="*.py"`8990* `export_types = [ExportType.CSV, ...]` becomes `exporters = ["csv", ...]`. Built-ins: `csv`, `json`, `tsv`, `xlsx`, `ods`, `html`, `yaml`, `pdf`. Remove the `ExportType` import.91* `export_fields` (include list) becomes `exclude_fields_from_export` (exclude list). Invert the field list against the view's `fields`.92* New capabilities to mention, not auto-add: `importers`, `exclude_fields_from_import`, per-field `exclude_from_export` / `exclude_from_import`, `ExportConfig` / `ImportConfig` on `Admin`.9394## Step 7: Custom fields and template overrides9596Detect: `grep -rn "display_template\|form_template\|displays/\|forms/" --include="*.py"` and look for template dirs containing `displays/` or `forms/`.9798| Before | After |99| --- | --- |100| `templates/displays/*.html` | `templates/fields/detail/*.html` |101| `templates/forms/*.html` | `templates/fields/form/*.html` |102| client-side render function | `templates/fields/list/*.html` |103| `BaseField.display_template` | `BaseField.detail_template` |104| `form_template` path prefix | `fields/form/` |105106Move the template files and update the attribute paths together.107108## Step 8: CustomView109110Detect: `grep -rn "CustomView" --include="*.py"`111112`template_path` and `methods` are removed. Two replacement patterns:113114* Simple page: pass a `widget=` (for example `StatWidget`, `ChartWidget`, `TableWidget`).115* Full control: subclass `CustomView`, set `menu_label` and `path`, and declare endpoints with `@route`. Render with `self.templates.TemplateResponse(request=request, name=...)`.116117Pick based on what the old template did; when unclear, subclass with `@route` since it can render the existing template unchanged.118119## Step 9: Custom backends120121Detect: `grep -rn "BaseModelView" --include="*.py"` (subclasses implementing `find_all` / `count`).122123* `where` splits into `q` (full-text search string) and `filters` (typed `FilterGroup` tree).124* `order_by` (list of `"field direction"` strings) becomes `sorts` (list of `(field_name, direction)` tuples).125* Signatures: `find_all(self, request, skip=0, limit=100, q=None, sorts=None, filters=None)` and `count(self, request, q=None, filters=None)`.126127## Behavior changes to report128129Include these in the final summary even when no code change is needed:130131* Datetimes now display in the viewer's local timezone by default.132* Old bookmarked list URLs fall back to default list state; DataTables saved states are not migrated.133* `EmailField` validates server-side when `email-validator` is installed.134* CSRF protection is built-in and cookie-based. Remove any custom CSRF middleware wrapping the admin, and confirm `secret_key` is set.135136## Needs human decision137138Ask the user instead of guessing when you find:139140* JavaScript render functions or DataTables plugins (Step 3): the ported template needs their review.141* `datatables_options` carrying meaningful config (custom ordering, page lengths): confirm the server-side equivalent they want.142* Odmantic usage: version pin versus Beanie migration.143* Missing `secret_key` source: which env var or settings entry to use.