Plugins
A Plugin is the only sanctioned extension point in fastauth. Subclass
fastauth.plugins.base.Plugin, set the id class variable, and override only
the hooks you need:
from typing import ClassVar
from collections.abc import Sequence
from datetime import timedelta
from pydantic import SecretStr
from fastauth import FastAuth, FastAuthOptions
from fastauth.database import memory
from fastauth.domain.models import WireModel
from fastauth.plugins.base import Capability, EndpointSpec, Plugin
from fastauth import email_password
class HelloResponse(WireModel):
message: str
class HelloPlugin(Plugin):
id: ClassVar[str] = "myapp-hello"
def capabilities(self) -> Sequence[Capability]:
return [
Capability(
id="myapp.hello",
description="Example hello-world extension.",
plugin_id=self.id,
)
]
def endpoints(self) -> Sequence[EndpointSpec]:
return [
EndpointSpec.get(
"/hello",
name="hello",
tags=["Hello"],
handler=self.hello,
response_model=HelloResponse,
)
]
async def hello(self) -> HelloResponse:
return HelloResponse(message="world")
auth = FastAuth(
FastAuthOptions(
secret_key=SecretStr("replace-me-with-your-application-secret"),
database=memory(),
),
plugins=[email_password(), HelloPlugin()],
)
PluginRegistry validates ids and aggregates endpoints(),
capabilities(), event_handlers(), trusted_origins(), and
rate_limit_rules() across the installed plugins. Lifespan hooks
(lifespan_startup, lifespan_shutdown) start in registration order and shut
down in reverse order. If startup fails, already-started plugins are shut down
before the error is reported.
EndpointSpec is only an HTTP route descriptor: method, path, name, tags,
handler, and response model. Request bodies are inferred from handler type
annotations. It does not declare authentication or rate-limit behavior. Plugin
handlers should enforce authentication with
self.require_session(request), and plugins should contribute rate limits
through rate_limit_rules().
Plugin SDK contract
A production plugin should declare every surface it contributes:
capabilities()for discoverable runtime features.endpoints()for HTTP routes.server_api_name()andserver_api()for typed server-side plugin APIs.event_handlers()orbind(context).event_bus.subscribe(...)for event subscribers.rate_limit_rules()for route-specific limits.trusted_origins()for callback origins that should pass CSRF origin checks.lifespan_startup()/lifespan_shutdown()for managed external resources.schemas()for additive MongoDB/Postgres collections, tables, fields, and indexes owned by the plugin.
Applications can inspect installed capabilities through auth.capabilities.
Prefer typed constants for first-party features:
from fastauth import EMAIL_PASSWORD, USERNAME_SIGN_IN
if auth.capabilities.has(USERNAME_SIGN_IN):
...
auth.capabilities.require(EMAIL_PASSWORD)
auth.plugin_info() returns typed plugin metadata for diagnostics, generated
docs, and plugin conformance tests. Endpoint metadata is exposed as
EndpointInfo, a serializable DTO containing method, path, name, tags, and
model names. It does not expose live handler callables.
Plugin surfaces are snapshotted when FastAuth builds its PluginRegistry.
Do not make endpoints(), capabilities(), or related declaration hooks
depend on mutable runtime state after construction.
Executable schemas
Plugin schemas are deterministic, additive declarations. Each plugin-owned
table or collection must have at least one migration marker before it can be
executed. Fastauth supports field types str, int, float, bool,
datetime, bytes, and json; arbitrary SQL, MongoDB commands, destructive
alterations, renames, and data migrations are intentionally excluded.
from fastauth.plugins import FieldSpec, IndexSpec, MigrationSpec, PluginSchema, TableSpec
def schemas(self):
return [
PluginSchema(
plugin_id=self.id,
tables=(
TableSpec(
name="webhook_deliveries",
fields=(
FieldSpec(name="id", python_type="str", unique=True),
FieldSpec(name="created_at", python_type="datetime", indexed=True),
),
indexes=(
IndexSpec(
name="webhook_deliveries_created_at_idx",
fields=("created_at",),
),
),
),
),
migrations=(
MigrationSpec(name="create_webhook_deliveries", version=1),
),
)
]
MongoDB and Postgres store a ledger with plugin id, migration name, version, schema fingerprint, and application time. Replaying a plan is idempotent; changing the latest recorded version in place is an error. Postgres applies DDL and ledger records transactionally under an advisory lock. MongoDB DDL is not transactionally equivalent, so its executor is retry-safe and converges after partial work.
Use plugin_migration_mode="apply", "check", or "disabled" on MongoDB or
Postgres database options. The default is apply in development and check
in production. For deploy jobs, load the application's plugin registry:
fastauth migrate \
--postgres-url postgresql+asyncpg://... \
--auth myapp.auth:auth \
--plugin-migrations apply
fastauth migrate-dry-run \
--backend postgres \
--auth myapp.auth:auth \
--plugin-migrations check
Plugin server APIs are exposed under auth.api.plugins by name and by plugin
id:
api = auth.api.plugins.by_name["my_plugin"]
same_api = auth.api.plugins.by_plugin_id["myapp-plugin"]
This keeps core auth.api.sign_in, auth.api.session, auth.api.password,
and auth.api.user stable while letting plugins publish their own command /
result based server API.
server_api() is called after FastAuth binds the plugin to AuthContext, so
plugin API objects may read self.require_context() or capabilities during
construction. Keep declaration hooks such as endpoints() and
capabilities() context-free; those are snapshotted before binding.
Authoring template
Use this shape for plugins that need fastauth context, authentication, optional storage capabilities, and route-specific rate limits:
from collections.abc import Sequence
from typing import ClassVar
from fastapi import Request
from fastauth.domain.models import WireModel
from fastauth.plugins.base import Capability, EndpointSpec, Plugin, RateLimitRule
from fastauth.runtime.context import AuthContext
from fastauth.storage.base import AuditLogStore
class MyPluginResponse(WireModel):
user_id: str
class MyPluginServerApi:
async def ping(self) -> MyPluginResponse:
return MyPluginResponse(user_id="system")
class MyPlugin(Plugin):
id: ClassVar[str] = "myapp-plugin"
def __init__(self) -> None:
self.audit_logs: AuditLogStore | None = None
def capabilities(self) -> Sequence[Capability]:
return [
Capability(
id="myapp.plugin",
description="Example authenticated plugin capability.",
plugin_id=self.id,
)
]
def server_api_name(self) -> str:
return "my_plugin"
def server_api(self) -> object:
return MyPluginServerApi()
def bind(self, context: AuthContext) -> None:
super().bind(context)
self.audit_logs = self.require_capability(AuditLogStore)
def endpoints(self) -> Sequence[EndpointSpec]:
return [
EndpointSpec.get(
"/my-plugin/me",
name="my_plugin_me",
tags=["MyPlugin"],
handler=self.me_handler,
response_model=MyPluginResponse,
)
]
def rate_limit_rules(self) -> Sequence[RateLimitRule]:
return [
RateLimitRule(
path="/my-plugin/me",
window=timedelta(seconds=60),
max_requests=30,
)
]
async def me_handler(self, request: Request) -> MyPluginResponse:
session = await self.require_session(request)
return MyPluginResponse(user_id=session.user.id)
The important rules are:
- Use
bind(context)plussuper().bind(context)for startup validation and for storingAuthContext. - Use
self.require_capability(SomeStoreProtocol)before enabling a plugin that needs optional storage. - Authenticate in the handler with
await self.require_session(request). - Add route-specific limits with
rate_limit_rules()rather than extra fields onEndpointSpec.