Architecture
Component / Controller / Application pattern
HexRift is structured around a lightweight Component/Controller/Application framework in hexrift/core/.
BaseApplication (singleton registry + dependency injector)
└── registers Components in order:
├── SchemaComponent → SchemaController (app.schema)
├── DeriveComponent → DeriveController (app.derive)
├── KeysComponent → KeysController (app.keys)
└── RenderComponent → RenderController (app.render)
BaseApplication (hexrift/core/application.py)
- Singleton — only one instance exists at runtime.
- Iterates
default_componentson__init__, callingself.register(component_cls). register(...)instantiates the component, stores it inself.components, exposes controller attributes (when enabled), then runscomponent.on_register().- Exposes each controller as an attribute:
app.schema,app.derive,app.keys,app.render. - Holds a shared
rich.Consolefor all output.
BaseComponent (hexrift/core/component.py)
- Layer between Click CLI and business logic.
- Defines
name,controller_class, and optionallyexpose_controller. expose_cli(base: click.Group)registers Click commands on the main group at import time.
BaseController (hexrift/core/controller.py)
- Holds business logic; receives
appfor cross-component access. - Example:
RenderControllercallsself.app.schema.configandself.app.keys.load_node_keys(...).
Request flow
CLI command invoked
→ Click routes to component's expose_cli handler
→ handler accesses app (passed via ctx.obj)
→ app.schema loads YAML (cached after first load)
→ handler calls controller method
→ controller accesses other components via self.app.*
Deterministic derivation
All identifiers (UUIDs, shortIds, emails) are derived from the topology — never randomly generated. Re-running always produces the same output for the same namespace and names.
Source: hexrift/components/derive/identity.py — Namespace class.
UUID derivation
| Identifier | Formula |
|---|---|
| Namespace UUID | UUID5(UUID(int=0), namespace) |
| User UUID | UUID5(namespace_uuid, username) |
| Server UUID | UUID5(user_uuid, "{username}-server") |
| Guest UUID | UUID5(user_uuid, guest_label) |
| Portal UUID | UUID5(user_uuid, "{label}-portal") |
| Hub-Exit UUID | UUID5(namespace_uuid, "{hub_id}-{exit_id}") |
| Warp UUID | Hub-Exit UUID with 3rd segment replaced by ffff |
A user's UUID can be overridden in the YAML with users[].uuid.
ShortId derivation
ShortIds are the first 16 hex characters of a SHA-256 hash:
| Identifier | Input string |
|---|---|
| Group shortId | "{group_id}.{namespace}" (or override via groups[].short_id) |
| Hub shortId | "{node_id}.hub.{namespace}" |
| Exit shortId | "{node_id}.exit.{namespace}" |
| User shortId | "{username}.user.{namespace}" |
Email derivation
| Format | |
|---|---|
| User | {username}@{namespace} |
| Server | {username}-server@{username} |
| Portal | {label}-portal@{username} |
| Guest | {label}@{username} |
| Hub-Exit | {hub_id}-{exit_id}@{namespace} |
| Warp | warp-{hub_id}-{exit_id}@{namespace} |
Key storage
Keypairs are generated by gen-keys and stored in keys/<nodeId>.yaml.
Source: hexrift/components/keys/store.py, reality.py, decryption.py.
File format
reality_private_key: "<base64url-no-padding>" # x25519 private key (32 bytes)
reality_public_key: "<base64url-no-padding>" # x25519 public key (32 bytes)
decryption: "mlkem768x25519plus.rprx_vision.12h.{private_key_b64}" # server inbound
encryption: "mlkem768x25519plus.rprx_vision.0rtt.{public_key_b64}" # client outbound
Key string format
| String | Usage | Format |
|---|---|---|
decryption |
Xray server inbound | {method}.{mode}.{session_time}[.{padding}].{private_key_b64} |
encryption |
Client share URL | {method}.{mode}.0rtt.{public_key_b64} |
File permissions are set to 0o600 (owner read/write only).
Hub key sharing
Hub nodes in the same region share the same keypair. gen-keys detects this automatically and only generates one file.
Adding a component
Follow the pattern established by the existing four components:
1. Create module structure
hexrift/components/myfeature/
├── __init__.py
├── component.py # Click CLI registration
└── controller.py # Business logic
2. Define controller
# hexrift/components/myfeature/controller.py
from hexrift.core.controller import BaseController
class MyController(BaseController["HexRiftApp"]):
def do_something(self) -> None:
cfg = self.app.schema.config # access other components
...
3. Define component
# hexrift/components/myfeature/component.py
import rich_click as click
from hexrift.core.component import BaseComponent
from .controller import MyController
class MyComponent(BaseComponent["HexRiftApp", MyController]):
name = "myfeature"
controller_class = MyController
expose_controller = True
@classmethod
def expose_cli(cls, base: click.Group) -> None:
@base.command()
@click.pass_obj
def mycommand(app: "HexRiftApp") -> None:
"""One-line help text."""
app.myfeature.do_something()
4. Register in the app
# hexrift/app.py
from hexrift.components.myfeature.component import MyComponent
class HexRiftApp(BaseApplication["HexRiftApp"]):
default_components = [..., MyComponent]
myfeature: MyController
Developer commands
uv run ruff check . --fix # lint + auto-fix
uv run ruff format . # format
uv run ty check # type-check
uv run prek run --all-files # run all pre-commit hooks