Hop3's Plugin Architecture: Extensibility Without Complexity¶
When building a PaaS, you face a fundamental tension: you want to support many languages, databases, and deployment patterns, but you don't want a monolithic codebase that's impossible to maintain. Hop3 solves this with a plugin architecture built on Pluggy.
Why Plugins?¶
A PaaS needs to handle many concerns:
- Build: How do you compile Go? Install Python dependencies? Bundle JavaScript?
- Deploy: uWSGI for Python? PM2 for Node? Static files for SPAs?
- Services: PostgreSQL? MySQL? Redis? Each with its own setup and credentials
- Proxy: Nginx? Caddy? Traefik? Each with different configuration formats
- OS: Debian uses
apt, Fedora usesdnf, package names differ
Without plugins, you'd have massive switch statements and tightly coupled code. With plugins, each concern is isolated, testable, and replaceable.
Plugin Types¶
Hop3 defines six plugin types, each with a specific responsibility:
1. Builders¶
Builders orchestrate the build process. They decide where and how to build.
@dataclass(frozen=True)
class LocalBuilder:
"""Build directly on the server."""
name: str = "local"
def build(self, app: App, toolchain: LanguageToolchain) -> BuildArtifact:
return toolchain.build()
@dataclass(frozen=True)
class DockerBuilder:
"""Build inside a Docker container."""
name: str = "docker"
def build(self, app: App, toolchain: LanguageToolchain) -> BuildArtifact:
# Build in isolated container
...
2. Language Toolchains¶
Toolchains handle language-specific build logic:
class PythonToolchain(LanguageToolchain):
name = "Python"
def accept(self) -> bool:
# Detect Python projects
return self.check_exists(["requirements.txt", "pyproject.toml"])
def build(self) -> BuildArtifact:
self.make_virtual_env()
self.install_dependencies()
return self._make_build_artifact(kind="python")
Current toolchains: Python, Node.js, Ruby, Go, Rust, PHP, Java, Clojure, Static, Generic.
3. Deployers¶
Deployers run built applications:
@dataclass(frozen=True)
class UWSGIDeployer:
"""Deploy Python/Ruby apps via uWSGI."""
name: str = "uwsgi"
def accept(self, artifact: BuildArtifact) -> bool:
return artifact.kind in ("python", "ruby")
def deploy(self, app: App, artifact: BuildArtifact) -> None:
# Generate uWSGI config, start workers
...
Current deployers: uWSGI, Static, Docker Compose.
4. Addons¶
Addons provision backing services:
@dataclass(frozen=True)
class PostgreSQLAddon:
name: str = "postgres"
def create(self, instance_name: str) -> AddonCredential:
# Create database, user, return credentials
...
def attach(self, app: App, credential: AddonCredential) -> dict[str, str]:
# Return env vars to inject
return {"DATABASE_URL": credential.connection_url}
Current addons: PostgreSQL, MySQL, Redis.
5. Proxy Plugins¶
Proxy plugins configure reverse proxies:
@dataclass(frozen=True)
class NginxProxy:
name: str = "nginx"
def setup(self, app: App, config: ProxyConfig) -> None:
# Generate nginx server block
# Handle SSL certificates
# Reload nginx
...
Current proxies: Nginx, Caddy, Traefik.
6. OS Implementations¶
OS plugins handle system-level operations:
@dataclass(frozen=True)
class DebianOS:
name: str = "debian"
def install_packages(self, packages: list[str]) -> None:
subprocess.run(["apt-get", "install", "-y", *packages])
def get_service_manager(self) -> str:
return "systemd"
Current: Debian family (Ubuntu, Debian), Red Hat family (Fedora, Rocky, AlmaLinux), Arch, BSD, macOS.
Hook Specifications¶
Plugins register themselves via hook specifications defined in core/hookspecs.py:
class Hop3Spec:
@hookspec
def get_builders(self) -> list[type[Builder]]:
"""Return available builders."""
@hookspec
def get_language_toolchains(self) -> list[type[LanguageToolchain]]:
"""Return available language toolchains."""
@hookspec
def get_deployers(self) -> list[type[Deployer]]:
"""Return available deployers."""
@hookspec
def get_addons(self) -> list[type[Addon]]:
"""Return available addons."""
@hookspec
def get_proxies(self) -> list[type[ProxyPlugin]]:
"""Return available proxy plugins."""
@hookspec
def get_os_implementations(self) -> list[type[OSImplementation]]:
"""Return available OS implementations."""
Plugin Discovery¶
At startup, Hop3 discovers plugins via Pluggy's plugin manager:
pm = pluggy.PluginManager("hop3")
pm.add_hookspecs(Hop3Spec)
# Register built-in plugins
pm.register(BuiltinPlugins())
# Discover external plugins via entry points
pm.load_setuptools_entrypoints("hop3.plugins")
External plugins can register via pyproject.toml:
Dependency Injection with Dishka¶
Plugins often need access to shared services (database sessions, configuration, etc.). We use Dishka for dependency injection:
class RepositoryProvider(Provider):
@provide(scope=Scope.REQUEST)
def get_app_repository(self, session: AsyncSession) -> AppRepository:
return AppRepository(session=session)
# In a plugin
class MyAddon:
def create(self, app_repo: AppRepository) -> AddonCredential:
app = app_repo.get_by_name(self.app_name)
...
This keeps plugins decoupled from implementation details.
Writing Your Own Plugin¶
Here's a minimal example of a custom addon:
# my_addon/plugin.py
from dataclasses import dataclass
from hop3.core.protocols import Addon, AddonCredential
@dataclass(frozen=True)
class MyCustomAddon:
name: str = "my-service"
def create(self, instance_name: str) -> AddonCredential:
# Your provisioning logic here
return AddonCredential(
addon_type=self.name,
instance_name=instance_name,
credentials={"API_KEY": "..."},
)
def destroy(self, credential: AddonCredential) -> None:
# Cleanup logic
pass
# Register via hookimpl
import pluggy
hookimpl = pluggy.HookimplMarker("hop3")
class MyPlugin:
@hookimpl
def get_addons(self):
return [MyCustomAddon]
Benefits of This Architecture¶
- Isolation: Each plugin is self-contained and testable
- Extensibility: Add new languages/services without touching core code
- Flexibility: Swap implementations (Nginx → Caddy) via configuration
- Maintainability: Clear boundaries between concerns
- Community: External plugins without forking
Learn More¶
Related posts: Package Architecture explains how we split Hop3 into focused packages. The BuildArtifact Pattern shows how plugins produce portable build artifacts. For plugin development, see the Plugin Development Guide.