Lifecycle & proxy

At startup a module registers with the core; the core then proxies its routes and monitors its health through a heartbeat.

Registration #

The module calls the core's /internal/* routes (on 127.0.0.1:8080), protected by the X-Internal-Secret header. It sends its identity, its base_url, its routes, its sidebar entries, its subscribed events and its version. The core keeps the instance in memory and in the database (the core.module_instances table).

In practice, startup registration boils down to a single POST — in the language of your choice:

// Rust — reqwest
let core   = std::env::var("KC__CORE__URL")?;
let secret = std::env::var("KC__CORE__INTERNAL_SECRET")?;

reqwest::Client::new()
    .post(format!("{core}/internal/modules/register"))
    .header("X-Internal-Secret", secret)
    .json(&serde_json::json!({
        "module_id": "demo",
        "base_url":  "http://127.0.0.1:3190",
        "version":   "0.1.0"
    }))
    .send().await?;
<?php
// PHP — cURL
$core   = getenv('KC__CORE__URL');
$secret = getenv('KC__CORE__INTERNAL_SECRET');

$ch = curl_init("$core/internal/modules/register");
curl_setopt_array($ch, [
    CURLOPT_POST       => true,
    CURLOPT_HTTPHEADER => [
        "X-Internal-Secret: $secret",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "module_id" => "demo",
        "base_url"  => "http://127.0.0.1:3190",
        "version"   => "0.1.0",
    ]),
]);
curl_exec($ch);
# Python — requests
import os, requests

core   = os.environ["KC__CORE__URL"]
secret = os.environ["KC__CORE__INTERNAL_SECRET"]

requests.post(
    f"{core}/internal/modules/register",
    headers={"X-Internal-Secret": secret},
    json={
        "module_id": "demo",
        "base_url":  "http://127.0.0.1:3190",
        "version":   "0.1.0",
    },
)
// Go — net/http
core   := os.Getenv("KC__CORE__URL")
secret := os.Getenv("KC__CORE__INTERNAL_SECRET")

body, _ := json.Marshal(map[string]string{
    "module_id": "demo",
    "base_url":  "http://127.0.0.1:3190",
    "version":   "0.1.0",
})
req, _ := http.NewRequest("POST", core+"/internal/modules/register", bytes.NewReader(body))
req.Header.Set("X-Internal-Secret", secret)
req.Header.Set("Content-Type", "application/json")
http.DefaultClient.Do(req)
# Perl — LWP::UserAgent
use LWP::UserAgent;
use JSON::PP;

my $core   = $ENV{'KC__CORE__URL'};
my $secret = $ENV{'KC__CORE__INTERNAL_SECRET'};

my $ua = LWP::UserAgent->new;
$ua->post("$core/internal/modules/register",
    'X-Internal-Secret' => $secret,
    'Content-Type'      => 'application/json',
    Content => encode_json({
        module_id => "demo",
        base_url  => "http://127.0.0.1:3190",
        version   => "0.1.0",
    }),
);
Note

The secret and the core URL are handed to the process through the environment (KC__CORE__INTERNAL_SECRET, KC__CORE__URL). See Configuration.

Lifecycle #

starting ──► healthy ──(heartbeat manqué)──► degraded ──► stopped
  • heartbeat — the module periodically pings the core to stay healthy.
  • unregister — on clean shutdown, the module removes itself from the registry.

The reverse proxy #

The core intercepts /api/v1/<module_id>/* and rewrites it to http://127.0.0.1:<port>/*, injecting the authenticated identity:

Requête navigateur :  GET /api/v1/calendar/events
        ▼ (core : auth + réécriture)
Vers le module :      GET http://127.0.0.1:3102/events
  + X-Internal-Secret, X-Kubuno-User-Id, X-Kubuno-User-Role, X-Kubuno-User-Email
Tip

The module is never exposed directly: it only listens on 127.0.0.1, and everything goes through the core, which authenticates and rewrites.