Skip to content

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_components on __init__, calling self.register(component_cls).
  • register(...) instantiates the component, stores it in self.components, exposes controller attributes (when enabled), then runs component.on_register().
  • Exposes each controller as an attribute: app.schema, app.derive, app.keys, app.render.
  • Holds a shared rich.Console for all output.

BaseComponent (hexrift/core/component.py)

  • Layer between Click CLI and business logic.
  • Defines name, controller_class, and optionally expose_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 app for cross-component access.
  • Example: RenderController calls self.app.schema.config and self.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.pyNamespace 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

Email 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

Commit convention

<type>[scope]: <description>

Examples:
  feat(render): add CDN support
  fix(schema): validate unique vless_route values
  chore(release): bump version to 0.6.0