Add new application structure: - app/main.py - application entry point - app/plugins/ - plugin system with auth plugin: - api/ - REST API endpoints and schemas - authorization/ - auth policies, providers, hooks - domain/ - business logic (service, models, jwt, password) - injection/ - route injection and guards - ops/ - operational utilities - runtime/ - runtime configuration - security/ - middleware, CSRF, dependencies - storage/ - user repositories and models - app/static/ - static assets (scalar.js for API docs) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
13 KiB
app.plugins Design Overview
This document describes the current role of backend/app/plugins, its plugin design contract, dependency boundaries, and how the current auth plugin provides services with minimal intrusion into the host application.
1. Overall Role
app.plugins is the application-side plugin boundary.
Its purpose is not to implement a generic plugin marketplace. Instead, it provides a clear boundary inside app for separable business capabilities, so that a capability can:
- carry its own domain model, runtime state, and adapters inside the plugin
- interact with the host application only through a limited set of seams
- remain replaceable, removable, and extensible over time
The only real plugin currently implemented under this directory is auth.
The current direction is not “put all logic into app”. It is:
- the host application owns unified bootstrap, shared infrastructure, and top-level router assembly
- each plugin owns its own business contract, persistence definitions, runtime state, and outward-facing adapters
2. Plugin Design Contract
2.1 A plugin should carry its own implementation
The primary contract visible in the current codebase is:
A plugin’s own ORM, runtime, domain, and adapters should be implemented inside the plugin itself. Core business behavior should not be scattered into unrelated external modules.
The auth plugin already follows that pattern with a fairly complete internal structure:
domain- config, errors, JWT, password logic, domain models, service
storage- plugin-owned ORM models, repository contracts, and repository implementations
runtime- plugin-owned runtime config state
api- plugin-owned HTTP router and schemas
security- plugin-owned middleware, dependencies, CSRF logic, and LangGraph adapter
authorization- plugin-owned permission model, policy resolution, and hooks
injection- plugin-owned route-policy loading, injection, and validation
In other words, a plugin should be a self-contained capability module, not a bag of helpers.
2.2 The host app should provide shared infrastructure, not plugin internals
The current contract is not that every plugin must be fully infrastructure-independent.
It is:
- a plugin may reuse the application’s shared
engine,session_factory, FastAPI app, and router tree - but the plugin must still own its table definitions, repositories, runtime config, and business/auth behavior
This is stated explicitly in auth/plugin.toml:
storage.mode = "shared_infrastructure"- the plugin owns its storage definitions and repositories
- but it reuses the application’s shared persistence infrastructure
So the real rule is not “never reuse infrastructure”. The real rule is “do not outsource plugin business semantics to the rest of the app”.
2.3 Dependencies should remain one-way
The intended dependency direction in the current design is:
gateway / app bootstrap
-> plugin public adapters
-> plugin domain / storage / runtime
Not:
plugin domain
-> depends on app business modules
A plugin may depend on:
- shared persistence infrastructure
app.stateprovided by the host application- generic framework capabilities such as FastAPI / Starlette
But its core business rules should not depend on unrelated app business modules, otherwise hot-swappability becomes unrealistic.
3. The Current auth Plugin Structure
The current auth plugin is effectively a self-contained authentication and authorization package with its own models, services, and adapters.
3.1 domain
auth/domain owns:
config.py- auth-related configuration definition and loading
errors.py- error codes and response contracts
jwt.py- token encoding and decoding
password.py- password hashing and verification
models.py- auth domain models
service.pyAuthServiceas the core business service
AuthService depends only on the plugin’s own DbUserRepository plus the shared session factory. The auth business logic is not reimplemented in gateway.
3.2 storage
auth/storage clearly shows the “ORM is owned by the plugin” contract:
models.py- defines the plugin-owned
userstable model
- defines the plugin-owned
contracts.py- defines
User,UserCreate, andUserRepositoryProtocol
- defines
repositories.py- implements
DbUserRepository
- implements
The key point is:
- the plugin defines its own ORM model
- the plugin defines its own repository protocol
- the plugin implements its own repository
- external code only needs to provide a session or session factory
That is the minimal shared seam the boundary should preserve.
3.3 runtime
auth/runtime/config_state.py keeps plugin-owned runtime config state:
get_auth_config()set_auth_config()reset_auth_config()
This matters because runtime state is also part of the plugin boundary. If future plugins need their own caches, state holders, or feature flags, they should follow the same pattern and keep them inside the plugin.
3.4 adapters
The auth plugin exposes capability through four main adapter groups:
api/router.py- HTTP endpoints
security/*- middleware, dependencies, request-user resolution, actor-context bridge
authorization/*- capabilities, policy evaluators, auth hooks
injection/*- route-policy registry, guard injection, startup validation
These adapters all follow the same rule:
- entry-point behavior is defined inside the plugin
- the host app only assembles and wires it
4. How a Plugin Interacts with the Host App
4.1 The top-level router only includes plugin routers
app/gateway/router.py simply:
- imports
app.plugins.auth.api.router - calls
include_router(auth_router)
That means the host app integrates auth HTTP behavior by assembly, not by duplicating login/register logic in gateway.
4.2 registrar performs wiring, not takeover
In app/gateway/registrar.py, the host app mainly does this:
app.state.authz_hooks = build_authz_hooks()- loads and validates the route-policy registry
- calls
install_route_guards(app) - calls
app.add_middleware(CSRFMiddleware) - calls
app.add_middleware(AuthMiddleware)
So the host app only wires the plugin in:
- register middleware
- install route guards
- expose hooks and registries through
app.state
The actual auth logic, authz logic, and route-policy semantics still live inside the plugin.
4.3 The plugin reuses shared sessions, but still owns business repositories
In auth/security/dependencies.py:
- the plugin reads the shared session factory from
request.app.state.persistence.session_factory - constructs
DbUserRepositoryitself - constructs
AuthServiceitself
This is a good low-intrusion seam:
- the outside world provides only shared infrastructure handles
- the plugin decides how to instantiate its internal dependencies
5. Hot-Swappability and Low-Intrusion Principles
5.1 If a plugin serves other modules, it should minimize intrusion
When a plugin provides services to the rest of the app, the preferred patterns are:
- expose a router
- expose middleware or dependencies
- expose hooks or protocols
- inject a small number of shared objects through
app.state - use config-driven route policies or capabilities instead of hardcoding checks inside business routes
Patterns to avoid:
- large plugin-specific branches spread across
gateway - unrelated business modules importing plugin ORM internals and rebuilding plugin logic themselves
- plugin state being maintained across many global modules
5.2 Low-intrusion seams already visible in auth
The current auth plugin already uses four important low-intrusion seams:
- router integration
gateway.routeronly callsinclude_router
- middleware integration
registraronly registersAuthMiddlewareandCSRFMiddleware
- policy injection
install_route_guards(app)appendsDepends(enforce_route_policy)uniformly to routes
- hook seam
authz_hooksis exposed viaapp.state, so permission providers and policy builders can be replaced
This structure has three practical benefits:
- host-app changes stay concentrated in the assembly layer
- plugin core logic stays concentrated inside the plugin directory
- swapping implementations does not require editing business routes one by one
5.3 Route policy is a key low-intrusion mechanism
auth/injection/registry_loader.py, validation.py, and route_injector.py together form an important contract:
- route policies live in the plugin-owned
route_policies.yaml - startup validates that policy entries and real routes stay aligned
- guards are attached by uniform injection instead of manual per-endpoint code
That allows the plugin to:
- describe which routes are public, which capabilities are required, and which owner policies apply
- avoid large invasive changes to the host routing layer
- remain easier to replace or trim down later
6. What “ORM and runtime are implemented inside the plugin” Should Mean
That contract should be read as three concrete rules:
- data models belong to the plugin
- the plugin’s own tables, Pydantic contracts, repository protocols, and repository implementations stay inside the plugin directory
- runtime state belongs to the plugin
- plugin-owned config caches, context bridges, and plugin-level hooks stay inside the plugin
- the outside world exposes infrastructure, not plugin semantics
- for example shared
session_factory, FastAPI app, andapp.state
- for example shared
Using auth as the example:
- the
userstable is defined inside the plugin, not inapp.infra AuthServiceis implemented inside the plugin, not ingatewayget_auth_config()is maintained inside the plugin, not cached elsewhereAuthMiddleware,route_guard, andAuthzHooksare all provided by the plugin itself
This is the structural prerequisite for meaningful pluginization later.
7. Current Scope and Non-Goals
At the current stage, the role of app.plugins is mainly:
- to create module boundaries for separable application-side capabilities
- to let each plugin own its own domain/storage/runtime/adapters
- to connect plugins to the host app through assembly-oriented seams
The current non-goals are also clear:
- this is not yet a full generic plugin discovery/installation system
- plugins are not dynamically enabled or disabled at runtime
- shared infrastructure is not being duplicated into every plugin
So at this stage, “hot-swappable” should be interpreted more precisely as:
- plugin boundaries stay as independent as possible
- integration points stay concentrated in the assembly layer
- replacing or removing a plugin should mostly affect a small number of places such as
registrar, router includes, andapp.statehooks
8. Suggested Evolution Rules
If app.plugins is going to become a more stable plugin boundary, the codebase should keep following these rules:
- each plugin directory should keep a
domain/storage/runtime/adaptersplit - plugin-owned ORM and repositories should not drift into shared business directories
- when a plugin serves the rest of the app, it should prefer exposing protocols, hooks, routers, and middleware over forcing external code to import internal implementation details
- seams between a plugin and the host app should stay mostly limited to:
router.include_router(...)app.add_middleware(...)app.state.*- lifespan/bootstrap wiring
- config-driven integration should be preferred over scattered hardcoded integration
- startup validation should be preferred over implicit runtime failure
9. Summary
The current app.plugins contract can be summarized in one sentence:
Each plugin owns its own business implementation, ORM, and runtime; the host application provides shared infrastructure and assembly seams; and services should be integrated through low-intrusion, replaceable boundaries so the system can evolve toward real hot-swappability.