Plugins
A plugin is a Python package that extends starlette-admin through a single constructor argument. A plugin can bundle any combination of fields, templates, static assets, model converters, filters, import/export formats, storage backends, event subscribers, views, routes, middlewares, theme assets, and translation catalogs.
Using a plugin
Pass plugins through the plugins argument when you construct your Admin instance:
from starlette_admin_geospatial import GeospatialPlugin
from starlette_admin.contrib.sqla import Admin
admin = Admin(engine, plugins=[GeospatialPlugin(default_zoom=13)])
The plugin constructor takes the options, and the list goes straight to Admin. Nothing else to set up or register. Options flow from the constructor down to the Python backend, the Jinja templates, and the frontend JavaScript.
Building a plugin
To write a plugin, start from the official cookiecutter template. It generates a publishable package with the right directory structure and configuration.
Prerequisites
Install cookiecutter with your package manager. See the official installation guide for the details:
Scaffolding
Run the cookiecutter template from any location:
The template prompts you for the plugin name, package slug, version, and a few other variables. When it finishes, you have a self-contained package with:
- A
src/directory holding your plugin class and fields. - Correctly namespaced
templates/,static/, andtranslations/folders. - A complete test suite.
- A runnable example application.
The plugin API
At the core of every plugin is a subclass of BasePlugin (starlette_admin.plugins.BasePlugin), which gives you hooks to register your features while Admin initializes.
The name attribute is a unique kebab-case identifier that doubles as the namespace for your templates and static assets. Every template and static file your plugin ships has to live under plugins/<name>/.
Asset folders
A plugin can carry exactly three folders at the root of its package. There's nothing to register, because the admin finds them by convention:
templates/: Jinja templates, which have to sit undertemplates/plugins/<name>/.static/: Static assets such as CSS and JS files, which have to sit understatic/plugins/<name>/.translations/: Babel translation catalogs.
Staying inside the plugins/<name>/ namespace keeps your assets from colliding with core files or other plugins, while leaving them overridable through the user's own templates_dir or static_dir.
Declarative hooks
Override the declarative hooks to inject assets, register views, or mount routes.
css_links(self, request: Request) -> Sequence[str]: Adds stylesheets to every admin page layout.js_links(self, request: Request) -> Sequence[str]: Adds scripts to every admin page layout.views(self) -> Sequence[BaseView]: Returns the views to register in the admin sidebar. Return aDropDownto group them.routes(self) -> Sequence[Route | Mount]: Returns headless endpoints mounted under/plugins/<name>/, which is handy for webhooks and proxy endpoints.middlewares(self) -> Sequence[Middleware]: Adds Starlette middlewares.template_globals(self) -> dict[str, Any]: Exposes Jinja globals, prefixed with<name>_so they can't collide.template_filters(self) -> dict[str, Callable]: Exposes Jinja filters, prefixed with<name>_the same way.
The setup hook
setup(self, admin: BaseAdmin) -> None integrates your plugin with the core registries. Use it to register model converters, filters, import and export formats, storage backends, and event subscribers. It runs after the declarative hooks are applied.
The lifecycle hook
on_mount(self, admin: BaseAdmin) -> None runs exactly once, after the Starlette sub-application is built and mounted. The built application is available as admin.app.
Templates and overrides
Plugin templates join the loader chain automatically. A user overrides one by putting a file at the matching path inside their own templates_dir, which always takes priority. To override plugins/geospatial/fields/form/point.html, for example, they create templates_dir/plugins/geospatial/fields/form/point.html.
So that a user override can extend the original safely, every plugin gets a @<name> prefix mapping that works like the @core prefix. The override starts with {% extends "@geospatial/fields/form/point.html" %} and extends the base plugin template without including itself recursively.
Frontend JavaScript integration
A plugin that ships custom fields should package its frontend scripts according to the field initializer contract. That keeps them working across both full page loads and dynamically inserted fragments.
- Target locally: Query inside the
containerelement you're given, never the globaldocument. - Be idempotent: Core runs the initializer on DOM ready and again whenever it inserts inline rows or fragments.
- Use data attributes: Read configuration from the
data-*attributes rendered on the field element.
(function () {
function initSlider(container) {
var input = container.querySelector('input[type="range"]');
var output = container.querySelector(".sa-slider-output");
var suffix = container.dataset.suffix || "";
input.addEventListener("input", function () {
output.textContent = input.value + suffix;
});
}
// Register the initializer so core runs it on the right lifecycle events
window.StarletteAdmin.registerFieldInitializer(function (element) {
element.querySelectorAll("[data-sa-slider]").forEach(initSlider);
});
})();
Extension points via the setup hook
Plugins use the existing public registries rather than a separate extension path of their own.
- Converters: Call
register_converter, from the contrib backend you're targeting, to map ORM column types to your field classes. Define the field itself as an ordinaryStringFieldsubclass, storing and displaying geometries as WKT text:
from dataclasses import dataclass
from typing import Any
from starlette_admin.contrib.sqla.converters import register_converter
from starlette_admin.fields import StringField
@dataclass
class MyGeoField(StringField):
...
@register_converter("Geometry")
def convert_geometry(*args: Any, **kwargs: Any) -> MyGeoField:
return MyGeoField(*args, **kwargs)
- Filters: Call
register_filtersto attach filter classes to a field type.
from starlette_admin.contrib.sqla.filters import register_filters
register_filters(MyGeoField, WithinBoundingBoxFilter)
- Storage: Call
register_storageto expose a new backend, such as Azure or GCS.
- Importers and Exporters: Use
register_import_formatandregister_export_format.
from starlette_admin.export import register_export_format
register_export_format("pdf", PDFExporter())
A plugin can support several ORM backends, so import them conditionally inside setup(). That way the plugin still loads when the user installed only one of them:
def setup(self, admin: "BaseAdmin") -> None:
try:
from starlette_admin_geospatial.contrib.sqla import register_sqla_converters
register_sqla_converters()
except ImportError:
pass # geoalchemy2 or sqlalchemy not installed
What's next
- Custom Themes: Package and share a full visual system, using the same cookiecutter workflow.
- Events: The subscriber API a plugin registers from its
setup()hook. - Extension Points: Every registry and base class a plugin can hook into.