> ## Documentation Index
> Fetch the complete documentation index at: https://metoro.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Installation Planning

> Plan topology, networking, identity, dependencies, and ClickHouse storage before installing Metoro

export const usePlannerSelections = () => {
  const [selections, setSelections] = useState(defaultSelections);
  useEffect(() => {
    setSelections(readSelections());
    const handleChange = event => {
      setSelections({
        ...defaultSelections,
        ...event.detail
      });
    };
    window.addEventListener(eventName, handleChange);
    return () => window.removeEventListener(eventName, handleChange);
  }, []);
  return [selections, setSelections];
};

export const updateStoredSelection = (section, value) => {
  const nextSelections = {
    ...readSelections(),
    [section]: value
  };
  window.localStorage.setItem(storageKey, JSON.stringify(nextSelections));
  window.dispatchEvent(new window.CustomEvent(eventName, {
    detail: nextSelections
  }));
  return nextSelections;
};

export const storageKey = "metoro-onprem-installation-planner-v1";

export const selectedDecisionItems = selections => [...sectionOrder.map(section => ({
  title: sections[section].title,
  label: optionLabel(section, selections[section])
})), {
  title: "Deployment hostname",
  label: cleanHostname(selections.deploymentHostname) || "Not set"
}, ...selections.network === "splitHosts" ? [{
  title: "Ingester hostname",
  label: cleanHostname(selections.ingesterHostname) || "Not set"
}] : []];

export const sections = {
  topology: {
    title: "Topology",
    prompt: "Where will the hub run?",
    options: [{
      id: "dedicated",
      label: "Dedicated hub cluster",
      recommended: true,
      description: "Run the hub in its own cluster and connect monitored clusters outbound to it."
    }, {
      id: "shared",
      label: "Shared workload cluster",
      description: "Run the hub in the same cluster as monitored workloads for smaller deployments."
    }]
  },
  network: {
    title: "Network access",
    prompt: "How will ingress be exposed?",
    options: [{
      id: "sharedHost",
      label: "Shared host",
      recommended: true,
      description: "Use one deploymentUrl host with path routing for UI/API and ingestion."
    }, {
      id: "splitHosts",
      label: "Split hosts",
      description: "Use separate apiserver and ingester hostnames when routing or ownership differs."
    }]
  },
  urlScheme: {
    title: "URL scheme",
    prompt: "Will the ingress use TLS?",
    options: [{
      id: "https",
      label: "HTTPS",
      recommended: true,
      description: "Use https:// URLs and configure TLS for the browser-facing hub host."
    }, {
      id: "http",
      label: "HTTP",
      description: "Use http:// URLs when the browser-facing hub host is not served with TLS."
    }]
  },
  tls: {
    title: "TLS and trusted CAs",
    prompt: "Which certificate trust path will be used?",
    options: [{
      id: "publicCa",
      label: "Public CA",
      recommended: true,
      description: "Use certificates already trusted by browsers and container images."
    }, {
      id: "privateCa",
      label: "Private CA bundle required",
      description: "Make the CA bundle available before installing the hub and monitored-cluster exporters."
    }]
  },
  authentication: {
    title: "Authentication",
    prompt: "How will users authenticate?",
    options: [{
      id: "oidcGroups",
      label: "OIDC with group mappings",
      recommended: true,
      description: "Prepare issuer, client, callback URL, scopes, group claim, and role mappings."
    }, {
      id: "emailPassword",
      label: "Email/password local users",
      description: "Use the bootstrap admin and locally managed users without an external identity provider."
    }]
  },
  dependencies: {
    title: "Dependencies",
    prompt: "Who will run PostgreSQL and ClickHouse?",
    options: [{
      id: "bundled",
      label: "Bundled dependencies and operators",
      recommended: true,
      description: "Let the hub chart install the datastore operators and manage PostgreSQL, ClickHouse, Temporal, and datastore resources."
    }, {
      id: "preinstalledOperators",
      label: "Bundled dependencies, operators managed in house",
      description: "Use centrally managed CloudNativePG and Altinity operators while the hub chart manages its datastore resources."
    }, {
      id: "external",
      label: "Externally managed ClickHouse and PostgreSQL",
      description: "Use customer-managed datastores with existing operational ownership."
    }]
  },
  clickhouseStorage: {
    title: "ClickHouse storage",
    prompt: "Where will ClickHouse keep telemetry data?",
    options: [{
      id: "hotPvcObjectStorage",
      label: "PVCs + object storage",
      recommended: true,
      description: "Use retained PVCs for about one day of hot data and object storage for longer-term telemetry."
    }, {
      id: "persistentVolumesOnly",
      label: "PVCs only",
      description: "Keep all ClickHouse telemetry on retained persistent volumes."
    }]
  }
};

export const sectionOrder = ["topology", "network", "urlScheme", "tls", "authentication", "dependencies", "clickhouseStorage"];

export const readSelections = () => {
  if (typeof window === "undefined") {
    return defaultSelections;
  }
  try {
    const stored = window.localStorage.getItem(storageKey);
    if (!stored) {
      return defaultSelections;
    }
    return {
      ...defaultSelections,
      ...JSON.parse(stored)
    };
  } catch {
    return defaultSelections;
  }
};

export const optionLabel = (section, value) => {
  const config = sections[section];
  const option = config.options.find(item => item.id === value) || config.options.find(item => item.id === defaultSelections[section]);
  return option.label;
};

export const ingesterUrl = selections => {
  return `${deploymentScheme(selections)}://${ingesterHostname(selections)}`;
};

export const ingesterHostname = selections => {
  if (selections.network !== "splitHosts") {
    return deploymentHostname(selections);
  }
  return cleanHostname(selections.ingesterHostname) || "CHANGE_ME_INGESTER_HOSTNAME";
};

export const eventName = "metoro-onprem-installation-planner-change";

export const deploymentUrl = selections => {
  return `${deploymentScheme(selections)}://${deploymentHostname(selections)}`;
};

export const deploymentHostname = selections => {
  return cleanHostname(selections.deploymentHostname) || "CHANGE_ME_METORO_HOSTNAME";
};

export const defaultSelections = {
  topology: "dedicated",
  network: "sharedHost",
  urlScheme: "https",
  tls: "publicCa",
  authentication: "oidcGroups",
  dependencies: "bundled",
  clickhouseStorage: "hotPvcObjectStorage",
  deploymentHostname: "",
  ingesterHostname: ""
};

export const cleanHostname = value => {
  return String(value || "").trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
};

export const DecisionsCard = ({selections, includePlanningLink = false}) => <>
    <div>
      <p>
        These choices are saved in this browser.
        {includePlanningLink ? <>
            {" "}
            Change them on the{" "}
            <a href="/docs/on-premises/10.x/installation/planning">
              Installation Planning page
            </a>
            .
          </> : " They will carry forward to Install Hub."}
      </p>
    </div>

    <ul>
      {selectedDecisionItems(selections).map(item => <li key={item.title}>
          <strong>{item.title}:</strong> {item.label}
        </li>)}
    </ul>
  </>;

export const InstallationPlannerDecisions = () => {
  const [selections] = usePlannerSelections();
  return <DecisionsCard selections={selections} />;
};

export const InstallationPlannerSelector = ({section}) => {
  const [selections, setSelections] = usePlannerSelections();
  const config = sections[section];
  if (!config) {
    return null;
  }
  const selectedValue = selections[section] || defaultSelections[section];
  const updateSelection = value => {
    setSelections(updateStoredSelection(section, value));
  };
  return <div className="my-5 rounded-lg border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900/40">
      <div className="mb-3 text-sm font-semibold text-zinc-950 dark:text-zinc-50">
        {config.prompt}
      </div>
      <div className="grid gap-3 md:grid-cols-2">
        {config.options.map(option => {
    const checked = selectedValue === option.id;
    return <label key={option.id} className={`block cursor-pointer rounded-md border p-3 transition ${checked ? "border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-950/30" : "border-zinc-200 bg-white hover:border-zinc-300 dark:border-zinc-800 dark:bg-zinc-950/40 dark:hover:border-zinc-700"}`}>
              <div className="flex items-start gap-3">
                <input type="radio" name={`installation-planner-${section}`} value={option.id} checked={checked} onChange={() => updateSelection(option.id)} className="mt-1" aria-label={`${config.title}: ${option.label}`} />
                <span>
                  <span className="block text-sm font-medium text-zinc-950 dark:text-zinc-50">
                    {option.label}
                    {option.recommended ? <span className="ml-2 rounded bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900/50 dark:text-blue-200">
                        Recommended
                      </span> : null}
                  </span>
                  <span className="mt-1 block text-sm text-zinc-700 dark:text-zinc-300">
                    {option.description}
                  </span>
                </span>
              </div>
            </label>;
  })}
      </div>
    </div>;
};

export const InstallationPlannerHostnames = () => {
  const [selections, setSelections] = usePlannerSelections();
  const updateInput = (key, value) => {
    setSelections(updateStoredSelection(key, value));
  };
  return <div className="my-5 rounded-lg border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900/40">
      <div className="mb-3 text-sm font-semibold text-zinc-950 dark:text-zinc-50">
        Which hostnames will DNS point at the ingress?
      </div>
      <div className="grid gap-3 md:grid-cols-2">
        <label className="block">
          <span className="block text-sm font-medium text-zinc-950 dark:text-zinc-50">
            UI and API hostname
          </span>
          <input type="text" value={selections.deploymentHostname || ""} onChange={event => updateInput("deploymentHostname", event.target.value)} placeholder="hub.your-domain.com" aria-label="UI and API hostname" className="mt-2 w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-950 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-50" />
          <span className="mt-1 block text-sm text-zinc-700 dark:text-zinc-300">
            Used for <code>deploymentUrl</code>, browser access, API traffic, and OIDC
            callback URLs.
          </span>
        </label>

        {selections.network === "splitHosts" ? <label className="block">
            <span className="block text-sm font-medium text-zinc-950 dark:text-zinc-50">
              Ingester hostname
            </span>
            <input type="text" value={selections.ingesterHostname || ""} onChange={event => updateInput("ingesterHostname", event.target.value)} placeholder="ingest.your-domain.com" aria-label="Ingester hostname" className="mt-2 w-full rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-950 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-50" />
            <span className="mt-1 block text-sm text-zinc-700 dark:text-zinc-300">
              Used for telemetry ingestion when split-host ingress is selected.
            </span>
          </label> : null}
      </div>
      <p className="mt-3 text-sm text-zinc-700 dark:text-zinc-300">
        Enter hostnames only. The URL scheme selected above controls whether
        generated URLs use HTTP or HTTPS.
      </p>
    </div>;
};

Make the installation decisions below before writing the hub values file, creating Secrets, configuring ingress, or onboarding monitored clusters.

## Topology

For production, the recommended topology is a <b>dedicated hub cluster</b>. The hub runs the UI, API, ingestion path, workflow workers, PostgreSQL, ClickHouse, and Temporal, so isolating it keeps observability infrastructure separate from the workload clusters it monitors.

```mermaid theme={null}
graph LR
    U[Users] --> HI[Hub ingress]

    subgraph HUB[Dedicated hub cluster]
        HI --> API[Apiserver]
        HI --> I[Ingester]
        API --> PG[(PostgreSQL)]
        API --> CH[(ClickHouse)]
        API --> T[Temporal]
        I --> CH
        T --> PG
    end

    subgraph MC1[Monitored cluster A]
        W1[Workloads] --> NA1[Node Agent]
        NA1 --> E1[Exporter]
    end

    subgraph MC2[Monitored cluster B]
        W2[Workloads] --> NA2[Node Agent]
        NA2 --> E2[Exporter]
    end

    E1 -->|Outbound telemetry and control plane| HI
    E2 -->|Outbound telemetry and control plane| HI
```

Multiple monitored clusters can send telemetry to one hub. This is the normal model for organizations that want one place to query telemetry, compare services across clusters, and operate a single set of stateful hub dependencies.

Running the hub in the same Kubernetes cluster as monitored workloads is acceptable for smaller or simpler deployments. It reduces the number of clusters to operate, but it also means hub upgrades, cluster maintenance, workload pressure, and telemetry collection all share the same failure domain.

```mermaid theme={null}
graph LR
    U[Users] --> HI[Hub ingress]

    subgraph CLUSTER[Shared Kubernetes cluster]
        subgraph HUB[Metoro Hub]
            HI --> API[Apiserver]
            HI --> I[Ingester]
            API --> PG[(PostgreSQL)]
            API --> CH[(ClickHouse)]
            API --> T[Temporal]
            I --> CH
            T --> PG
        end

        subgraph APP[Monitored workloads]
            W[Workloads] --> NA[Node Agent]
            NA --> E[Exporter]
        end

        E -->|Hub URL| HI
    end
```

Metoro <b>strongly</b> recommends the dedicated hub cluster model.

<InstallationPlannerSelector section="topology" />

## Network Access

Choose the network-facing base URL before installing. This will be the `deploymentUrl` in the helm chart later, and it should be a stable `http` or `https` URL with no path. It is used for the UI, API, OIDC callback configuration, and telemetry ingestion by default.

It is the single point of entry to interface with Metoro from outside the cluster.

```mermaid theme={null}
graph LR
    U[Users and browsers] --> DNS["External DNS for deploymentUrl"]
    MC[Monitored clusters] -->|"Resolve deploymentUrl"| DNS
    U -->|"OIDC login"| OIDC[OIDC provider]
    DNS -->|"Points at ingress address"| ING

    subgraph HUB[Hub cluster]
        ING[Ingress controller]
        ING -->|"UI and API paths"| API[Apiserver]
        ING -->|"Ingestion path"| I[Ingester]
    end

    MC -->|"Outbound API and telemetry"| ING
    API -->|"OIDC issuer checks"| OIDC
```

The next choice to make is split host or shared host for the apiserver and ingester components. A shared host can route UI/API traffic and ingestion traffic through different paths on the same hostname. Split host ingress uses separate hostnames for apiserver and ingester traffic, which can be useful when network policy, certificates, or load balancer ownership are managed separately.

<InstallationPlannerSelector section="network" />

<InstallationPlannerSelector section="urlScheme" />

<InstallationPlannerHostnames />

## Certificate Chains

The simplest path is a when all components are using signed certs with a certificate chain trusted by normal browsers and container images. In that case, no extra CA bundle is needed for hub workloads or monitored-cluster exporters.

If the hub URL, OIDC issuer, external database, or object-storage endpoint uses private PKI or a self-signed certificate, you need the shared CA bundle available before installation. The hub chart can mount that bundle from an existing ConfigMap through `trustedCAs`, adding it without replacing the image's default trust store.

<InstallationPlannerSelector section="tls" />

## Authentication

How will users access Metoro?

Metoro on-prem supports OIDC or local email/password accounts. OIDC is the recommended production path when an identity provider is available, because group mappings can place users into the right Metoro roles without managing every user directly in the hub.

Local email/password login is also supported, in this model admins create user account and distribute them to users.

<InstallationPlannerSelector section="authentication" />

## Bundled Or External Dependencies

Metoro has two main dependencies: ClickHouse and PostgreSQL.

In this section we decide how those are run.

Natively in Metoro, PostgreSQL is managed through the CloudNativePG operator, ClickHouse is managed through the Altinity ClickHouse Operator, and Temporal runs in the hub cluster with PostgreSQL persistence.

Metoro can install these operators as part of the installation process - meaning the whole dependency chain is managed through the metoro helm chart.

Some organizations manage Kubernetes operators centrally. In that model, the CloudNativePG and Altinity operators are installed and upgraded by the platform team, while the hub chart still creates and owns the PostgreSQL and ClickHouse resources that back Metoro.

Externally managed PostgreSQL and ClickHouse are also supported. Choose that path when your organization already has operational ownership, monitoring, and lifecycle management for those systems, and Metoro should connect to them rather than creating datastore resources in the hub cluster.

For production PostgreSQL CPU, memory, instance, and PVC starting points, see [PostgreSQL Sizing](/docs/on-premises/10.x/operations/postgresql/sizing) before finalizing the dependency model.

<InstallationPlannerSelector section="dependencies" />

## ClickHouse Storage

ClickHouse stores high volume data, so decide its storage model before sizing the deployment. The recommended path is to keep hot data on retained persistent volumes and move longer-term data to object storage. This allows data to be merged efficiently on fast block storage and then shipped to object storage when it is no longer hot for efficient long-term storage.

For production CPU, memory, PVC, local cache, and object-storage estimates, see [ClickHouse Sizing](/docs/on-premises/10.x/operations/clickhouse/sizing) before finalizing the storage model.

In the recommended model, plan persistent volumes for roughly one day of hot ClickHouse data, then prepare object storage for older telemetry. The object store endpoint must be reachable from the hub cluster, and any private CA used by that endpoint must be included in the trusted CA material prepared before installation.

PVC-only storage is simpler operationally because all ClickHouse data stays on Kubernetes persistent volumes. Choose it only when the cluster storage layer is sized for the full retention period and your operations team wants to avoid object storage in the first deployment.

If ClickHouse is externally managed, use the same decision to align with the team that owns that service. Metoro still needs to know whether recent and older telemetry are backed by separate storage tiers, even if the hub chart is not creating those resources.

<InstallationPlannerSelector section="clickhouseStorage" />

## Selected Decisions

These decisions are saved in this browser and appear on Install Hub.

<InstallationPlannerDecisions />
