Skip to content

NetBox modeling walkthrough — example.lan homelab

Approach

The exercise: take the example.lan lab as you've described it and figure out what gets modeled where. The output is three lists — what NetBox handles cleanly, what NetBox handles only with custom fields, what doesn't go in NetBox at all — and from those three lists, the data-model surface of the harness falls out.

Architectural premise (revisited from earlier in the conversation): NetBox is the source of truth for physical and L2/L3 inventory. It is not the source of truth for K8s state, Ceph state, ArgoCD-managed config, UniFi runtime config, or anything below the filesystem. The harness coordinates across those sources via adapters. Every time we hit something that doesn't fit in NetBox below, we're not finding a bug in NetBox — we're finding an adapter the harness needs.


What NetBox models cleanly

Sites and tenancy

Site: Covington
  description: Primary site, all compute, NAS, UniFi gear
  status: active
  facility: home

Site: Wyola
  description: Remote site reached via site-to-site VPN
  status: active
  facility: remote-home

Tenant: example.lan   # optional, only one tenant — skip if it adds noise

VLAN Group, VLANs, Prefixes

NetBox wants a VLANGroup to scope the VLAN namespace so VID collisions are detected. One group for the controller's L2 domain at Covington.

VLANGroup: example-covington
  scope: Site=Covington

VLANs (all in example-covington):
  vid=1   name=Primary    role=mgmt
  vid=4   name=DorkIOT    role=iot
  vid=6   name=Kubernet   role=compute
  vid=8   name=CovGuest   role=guest
  vid=10  name=CovVMs     role=vm-network

Prefixes:
  10.0.0.0/23  VLAN=Primary   role=mgmt           site=Covington
  10.0.4.0/23  VLAN=DorkIOT   role=iot            site=Covington
  10.0.6.0/23  VLAN=Kubernet  role=compute        site=Covington
  10.0.8.0/24  VLAN=CovGuest  role=guest          site=Covington
  10.0.10.0/24 VLAN=CovVMs    role=vm-network     site=Covington

VLAN 6 as a contiguous /23 just works — NetBox doesn't care that it spans two /24-shaped halves.

Inter-site VPN (NetBox 4.x Tunnels)

TunnelGroup: example-vpn

Tunnel: wyola-site-vpn
  group: example-vpn
  encapsulation: ipsec-transport   # or whatever UniFi actually uses
  status: active
  terminations:
    - Termination(role=hub, device=Covington-UDM, interface=wan1)
    - Termination(role=spoke, site=Wyola, ip=<wyola-public>)

WAN2 is a separate Circuit whose status reflects the long-running outage — see "where the model bends" for why this matters beyond just a status field.

Manufacturers and DeviceTypes

Manufacturers:
  ExampleVendor, RaspberryPiFoundation, Ubiquiti, MikroTik, Generic-MiniPC

DeviceTypes:
  ExampleVendor_MiniPC_Gen-A    (placeholder — node1/2/3 specifics TBD)
  ExampleBox    (node0)
  Generic_MiniPC_i5-6500T (node4/5/6 — likely a specific ExampleVendor/Minisforum SKU)
  ExampleVendor_NAS_Variant     (nas0)
  RaspberryPi_4B          (cp1/2/3, edgepi1)
  UDM_Pro_(UDMA6A8)       (Covington gateway)
  USW_Pro_Max_16_PoE      (dorkcore)
  USW_24_PoE_2.5G         (usw25office)
  USW_Flex_Mini           (uswjordan, uswlivingroom)
  U7_Pro                  (the APs — model varies, may need multiple types)
  Mikrotik_PTP_Radio      (the dark-infrastructure radios)

Device Roles

proxmox-node, k8s-control-plane, k8s-worker, k8s-worker-wol,
nas, usb-ip-server, gateway, switch-core, switch-access, ap, ptp-radio

k8s-worker-wol is its own role specifically to make WoL-aware queries trivial.

Devices

Eighteen-ish named devices. Listed with the fields that matter for downstream reasoning, not exhaustively:

Compute (Site=Covington):
  node0     role=proxmox-node           primary_ip=10.0.6.20/23
  node1     role=proxmox-node           primary_ip=10.0.6.21/23
  node2     role=proxmox-node           primary_ip=10.0.6.22/23
  node3     role=proxmox-node           primary_ip=10.0.6.23/23
  node4     role=k8s-worker-wol         primary_ip=10.0.6.24/23
  node5     role=k8s-worker-wol         primary_ip=10.0.6.25/23
  node6     role=k8s-worker             primary_ip=10.0.6.26/23
  cp1    role=k8s-control-plane      primary_ip=10.0.6.17/23
  cp2    role=k8s-control-plane      primary_ip=10.0.6.18/23
  cp3    role=k8s-control-plane      primary_ip=10.0.6.19/23
  edgepi1 role=usb-ip-server        primary_ip=10.0.0.53/23
  nas0    role=nas                    primary_ip=10.0.0.29/23   # see interface note

Network (Site=Covington):
  Covington   role=gateway              (UDM-class)
  dorkcore    role=switch-core
  usw25office role=switch-access
  uswjordan   role=switch-access
  uswlivingroom role=switch-access
  u7playroom  role=ap     ← also functional uplink for dorkcore
  u7officehall role=ap
  garage      role=ap
  dorkmesh    role=ap
  upstairs    role=ap

Network (Site=Wyola):
  <mikrotik-ptp-1>  role=ptp-radio    # exact device names tbd
  <mikrotik-ptp-2>  role=ptp-radio

Interfaces on key devices

The node/Pi interface set is minimal. nas0 is the tricky one:

nas0 interfaces:
  lan0   type=1000base-t       mac=<realtek-mac>    mode=access(vlan=1)
         ip=10.0.0.29/23
  lan6   type=10gbase-t        mac=<aquantia-mac>   mode=tagged(vlans=[6])
  br6    type=bridge           mac=<aquantia-mac>   # MUST equal lan6.mac
         bridge_members=[lan6]
         mode=access(vlan=6)
         ip=10.0.6.29/23

NetBox can express the bridge relationship and the MAC values, but the invariant (br6.mac == lan6.mac) lives elsewhere — see config assertions.

Clusters and VMs

ClusterType: proxmox-ve
ClusterType: kubernetes

Cluster: dorkprox
  type: proxmox-ve
  site: Covington
  members: node0, node1, node2, node3

Cluster: dorkk8s
  type: kubernetes
  site: Covington
  members: cp1, cp2, cp3, node4, node5, node6

VirtualMachines (cluster=dorkprox):
  pbs              # raw-passthrough to sda1 — see bend #5
  <other proxmox VMs you have>

VirtualMachines (cluster=dorkk8s):
  Not modeled per-pod in NetBox. K8s API is SoT.
  Optionally model "logical workloads" as VMs if you want a unified Services view —
  see "what lives outside NetBox" below.

Services

Top-of-mind services attached to whichever Device or VM serves them publicly:

Service(name=home-assistant,     device=<ha-vm>,            port=8123/tcp)
Service(name=authentik,          device=<authentik-pod>,    port=9000/tcp)
Service(name=frigate,            device=nas0,             port=5000/tcp)
Service(name=n8n,                device=<n8n-pod>,          port=5678/tcp)
Service(name=grafana,            device=<grafana-pod>,      port=3000/tcp)
Service(name=prometheus,         device=<prom-pod>,         port=9090/tcp)
Service(name=loki,               device=<loki-pod>,         port=3100/tcp)
Service(name=uptime-kuma,        device=<uk-pod>,           port=3001/tcp)
Service(name=minio,              device=nas0,             port=9000/tcp)
Service(name=guacamole,          device=<guac-pod>,         port=8080/tcp)
Service(name=nuclio,             device=<nuclio-pod>,       port=8070/tcp)
Service(name=pbs,                device=pbs-vm,             port=8007/tcp)
Service(name=nfs,                device=nas0,             port=2049/tcp)
Service(name=smb,                device=nas0,             port=445/tcp)
Service(name=tftp-pxe,           device=nas0,             port=69/udp)

The K8s-hosted services don't have stable "host" — they live on whatever pod the scheduler picked. Pointing them at a logical VirtualMachine(name=<service>-workload) and letting the K8s adapter keep local_context_data current is the cleanest pattern. Trying to keep Service.device pointing at the actual pod-host is a losing battle.

Cables — selectively

The one cable that earns its keep:

Cable(
  termination_a=dorkcore.uplink,
  termination_b=u7playroom.lan_port,
  label="homelab-chokepoint"
)

That edge is load-bearing for the harness's failure-domain reasoning. Everything else (which Pi is in which dorkcore port, which AP is connected to what) is documentation-only — model it if you enjoy that, skip it otherwise. Cable inventory is the single best example of "NetBox accuracy decays to 30% within 60 days" that the surveys keep finding; only model cables you'd actually keep current.


Where the model bends

Ten places, in rough order of architectural significance.

1. Parts with history (DIMMs, SSDs that migrated)

NetBox represents sub-components as InventoryItem records owned by a Device. There is no first-class identity that survives a move. When node4's 2×16 GB DIMMs went into node1, the NetBox-native operation is delete from node4, create on node1. Lineage gone. Same with the Pi SSDs → md0.

The fix is not a NetBox plugin. It's its own data model in the harness:

class PhysicalPart:
    id: UUID
    kind: Literal["dimm", "ssd", "hdd", "nvme", "nic", "psu", "gpu", ...]
    manufacturer: str
    model: str
    serial: str | None       # canonical identity when present
    capacity: int | None     # bytes
    speed: int | None        # MT/s for DIMM, MB/s for storage
    attributes: dict         # ECC, rank, NVMe gen, etc.
    notes: str

class Placement:
    part: PhysicalPart
    host: Device             # NetBox device ID
    slot: str                # "DIMM_A1", "/dev/nvme0n1", "USB-port-3"
    from_date: datetime
    to_date: datetime | None # null = currently placed
    confidence: Literal["verified", "inferred", "asserted"]
    source: str              # "dmidecode", "user", "import"

NetBox holds current placements only — as InventoryItems — and the harness keeps the full history. Reconciler updates NetBox when a part appears/disappears.

This model handles your lineage knot natively: the three SSDs in nas0.md0 have Placement rows whose previous entries point at cp1/2/3.

2. Expected-power-off (node4 and node5)

NetBox's Device.status has values like active, planned, staged, decommissioning, offline. None of them mean "intentionally asleep, will wake on demand." Adding a custom status is possible but conflates intent with state.

Cleaner shape:

Device.status               → intent: "is this device in service at all"
Device.cf_power_policy      → "always-on" | "wol-on-demand" | "manual"
Device.cf_expected_power_state → derived: "on" | "off" | "either"
Observation                 → actual: probed power state per discovery run

The discovery scheduler reads power_policy before pinging. The reconciler treats expected=off, observed=off as confirmation, not absence. The K8s adapter reads node labels and knows that homelab/always-on=false corresponds to power_policy=wol-on-demand.

3. Multi-source labels (K8s)

homelab/always-on=true and homelab/big-pods=true live in the K8s API. They affect placement (and therefore the planner's reasoning), but they're not NetBox's concern. NetBox should be able to display them via the K8s adapter populating Device.local_context_data, but the SoT remains the K8s API.

Pattern: any field whose canonical home is somewhere else gets stamped with cf_source = "k8s" | "proxmox" | "unifi" | "kernel-probe" | "user". NetBox is read-mostly for those fields; writes happen at the source.

4. MAC-pinned bridge invariant (nas0.br6)

NetBox stores br6.mac_address and lan6.mac_address as independent fields. Nothing enforces that they match, and the lab's correct operation requires it.

This is a ConfigurationAssertion:

ConfigurationAssertion(
  id=...,
  host="nas0",
  description="br6 bridge MAC must equal lan6 NIC MAC",
  rationale="DHCP reservation matches against bridge MAC; mismatch returns wrong IP",
  verifier_kind="ssh",
  verifier_command="ip link show br6 | awk '/link\\/ether/{print $2}' | "
                   "cmp -s - <(ip link show lan6 | awk '/link\\/ether/{print $2}')",
  expected_exit_code=0,
  artifact_link="ansible/roles/nas0-network/templates/br6.j2",
  last_verified=...,
  severity="high"
)

The harness runs the verifier on a schedule. If it fails, that's drift, and it goes into the reconciliation findings.

5. Raw-passthrough partition (nas0.sda1 → PBS VM)

NetBox can model: - nas0.sda as an InventoryItem - pbs-vm as a VirtualMachine - A VirtualDisk on pbs-vm

What it can't model is "the storage backing that VirtualDisk is this specific partition on this specific physical disk on this specific host."

Two reasonable options:

  • Cheap: custom field VirtualDisk.cf_backing_block_device = "nas0:/dev/disk/by-partlabel/proxmox-bs". Free-text, the planner parses it.
  • Better: a harness object StorageBacking(virtual_disk, host, block_device, mode=raw|qcow2|nfs|...) that the harness owns and updates from Proxmox API.

The planner needs the second one if it's going to reason about "if I migrate pbs-vm, where does its storage have to go" — which is exactly the kind of question this lab's planner should answer.

6. RAID-of-harvested-disks (nas0.md0)

NetBox doesn't model software RAID arrays as relationships between sub-components of a single device. You can shove md0 into an InventoryItem with a description and lose the structure, or you can let the harness own it:

StorageVolume(
  host="nas0",
  name="md0",
  kind="mdadm-raid5",
  capacity_bytes=894 * GiB,
  members=[PhysicalPart-id-x3],   # the three 447 GB SSDs
  attributes={"chunk_size": "512K", "layout": "left-symmetric", ...}
)

Combined with PhysicalPart + Placement history, you get the lineage knot for free: query members[*].placement_history and you see they used to live in cp1/2/3.

7. DNS split-brain

Same hostname can resolve to different IPs depending on whether the resolver is internal (UniFi gateway) or external (Cloudflare → ingress). NetBox FHRPGroup isn't this; IPAddress.dns_name is single-valued. The Services model attaches to a device but doesn't capture "this service has internal-via-x and external-via-y resolution paths."

Harness owns it:

ServiceEndpoint(
  service: Service,
  resolution_scope: "internal" | "external",
  hostname: str,
  ip: str,
  resolver: "unifi" | "cloudflare" | "consul" | ...,
  tls_provider: "cert-manager-dns01" | "self-signed" | None,
)

Each Service has 1..N endpoints. The harness's DNS adapter (UniFi for internal, Cloudflare for external) keeps them current. NetBox's Service becomes a thin pointer.

8. WAN2 down since March 2026 (no failover)

The static fact is modelable: WAN2 = Circuit(status=offline). The interesting parts aren't: - No failover configured — this is a configuration assertion that failed silently (or was never configured). Whether it's a bug or a design choice depends on intent. The harness needs both an assertion of intent ("dual WAN should fail over") and a verifier that checks the current UniFi config. - Outage has persisted for months — this is a finding worth surfacing, not just a status field. "WAN2 has been down ≥ 60 days; if this is permanent, decommission the circuit; if not, why hasn't it been fixed."

Generalizes: a static inventory store can't distinguish "this is the current state" from "this is the current state and that's been true for an unusually long time." The harness needs temporal awareness of inventory state to surface stale degradation as audit findings.

9. Dark infrastructure (Mikrotik PTP)

These devices are real, but no live source confirms them. NetBox holds them. The harness should mark them clearly:

Device.cf_discovery_source = "manual"
Device.cf_last_verified    = <date user last touched them>
Device.cf_verification_method = "user-asserted"

Audit rule: any device with discovery_source=manual and last_verified > 180 days gets flagged. Doesn't mean it's broken; means the harness can't vouch for it.

10. Topology chokepoint

The dorkcore → u7playroom uplink is a NetBox cable, as noted. The implication — every node in VLAN 6 depends on u7playroom for any traffic leaving the homelab subnet — is derived. The harness needs a topology-aware function: "for service S running on device D, enumerate the failure domains it depends on for (a) internal traffic, (b) internet egress, (c) cross-VLAN traffic." That function reads NetBox cables, traces paths, returns dependency sets.

That's downstream of NetBox modeling, but it's worth flagging now because it shapes which cables you model. Model the cables that affect failure-domain queries; skip the rest.


Custom fields to add on day one

Concrete list, scoped per object type. All can be added via NetBox admin without writing a plugin.

On Device:

cf_power_policy            choice: always-on / wol-on-demand / manual
cf_expected_power_state    choice: on / off / either           (derived)
cf_discovery_source        text: manual / unifi / proxmox / k8s / kernel-probe
cf_discovery_last_run      datetime
cf_last_verified           date
cf_capabilities            json: {"avx2":true,"iommu":true,"nvenc":["h264","hevc"],...}
cf_arch                    choice: amd64 / arm64 / arm / other
cf_hypervisor_type         choice: proxmox / esxi / kvm-host / docker-host / bare-metal / talos / none
cf_power_draw_idle_watts   decimal
cf_power_draw_max_watts    decimal

On VirtualMachine:

cf_arch                    choice: amd64 / arm64 / arm / other
cf_workload_intent_id      uuid → harness-owned WorkloadIntent
cf_deployment_artifact     text: pointer to ansible role / helm chart / git path

On Service:

cf_workload_profile_id     uuid → harness-owned WorkloadProfile
cf_endpoint_internal       text: internal hostname (resolves via UniFi)
cf_endpoint_external       text: external hostname (resolves via Cloudflare)
cf_sso_provider            choice: authentik-forward-auth / oauth / none / unknown

On Interface:

cf_pinned_mac_to           text: name of interface whose MAC this must match (for invariants)
cf_pxe_boot_target         boolean: is this interface a PXE-boot target

On Circuit:

cf_failover_partner        ref → other Circuit (for dual-WAN pairs)
cf_failover_configured     boolean
cf_outage_since            date

On Cable:

cf_chokepoint              boolean: cuts a failure domain — keep accurate

JSON custom fields are escape hatches. Use them for capability bags where the schema is still settling; promote frequently-queried fields to discrete typed fields once you know what you actually filter on.


Harness objects (not in NetBox)

These are the ones I'd ship as part of the harness's own data layer, with NetBox as one of several data sources behind them.

PhysicalPart           kind, mfr, model, serial, capacity, speed, attributes
Placement              part_id, host_id (→ NetBox), slot, from/to, confidence, source
StorageVolume          host, name, kind, capacity, members[part_id], attributes
StorageBacking         virtual_disk (→ NetBox VM disk), host, block_device, mode
ConfigurationAssertion host_or_scope, description, rationale, verifier, expected, severity
ServiceEndpoint        service (→ NetBox), scope, hostname, ip, resolver, tls
WorkloadIntent         user_request, target_arch_constraints, resource_estimate, status
WorkloadProfile        name, baseline_resources, scaling_shape, arch_support
DiscoveryRun           host, started_at, ended_at, probes[], privilege_level
Observation            run_id, probe, key, value, confidence, raw_payload_ref
ReconciliationFinding  kind, severity, description, evidence[], proposed_action
TrustBoundary          host, max_agent_authority, requires_user_approval[]

Most of these we've discussed in prior turns; bundling them here for completeness.


External sources of truth (not in NetBox at all)

Source Owns Adapter responsibility
K8s API live pod/node state, labels, taints, NotReady reasons populate Device.local_context_data; resolve service→host
Proxmox API VM state, Ceph OSD weights, replication health, snapshots populate VM status; surface Ceph health to harness
ArgoCD + Git declared application state (the 46 apps) feed planner with "what should be running"; diff vs K8s API for drift
UniFi controller DNS records, DHCP leases, switch port config, VLAN assignments populate internal DNS endpoints; verify cable terminations
Cloudflare API external DNS, ACME state populate external endpoints; check cert expiry
Kernel probes (SSH) actual storage layout, RAID state, NIC offloads, SMART ground truth that beats every management-plane DB (see nas0 lesson)
ZFS/mdadm/LVM commands array health, scrub state, resync progress flowed in via kernel probes
dmidecode DIMM slot topology, board, BIOS the only reliable source for memory layout

The harness coordinates across these. None of them are individually sufficient. None of them have to be NetBox.


Day-one reconciliation report

What the harness produces from the inventory above on its first full run, without being told anything specific to look for. Each item is a ReconciliationFinding:

[HIGH] CEPH-BOTTLENECK
  node0 has the largest RAM allocation (64 GB) in the Proxmox cluster but
  the lowest NIC speed (1 GbE). All other cluster members have ≥2.5 GbE.
  Ceph replication is bounded by the slowest peer.
  Options:
    (a) Reweight CRUSH map to reduce node0's primary-OSD share.
    (b) Add USB 2.5GbE NIC (~$25) dedicated to Ceph backend.
    (c) Move OSD off node0; use as compute-only.
    (d) Accept the cap, document it.

[HIGH] CONFIG-DRIFT-UNKNOWN
  e1000e TX-hang workaround deployed on node1, node2, node3 (Intel I219-V).
  node0 has the same NIC; workaround status unknown. Verify.

[HIGH] OUTAGE-STALE
  Circuit "WAN2" has been status=offline for ≥60 days (since March 2026).
  No failover from WAN1 to WAN2 is configured. Either:
    (a) Repair/replace WAN2 and configure failover.
    (b) Decommission the Circuit and dual-WAN expectation.
  Current state is "broken in a way nothing alerts on."

[MEDIUM] CHOKEPOINT
  dorkcore (homelab core switch) uplinks via u7playroom (AP). Single edge
  between VLAN 6 and the rest of the network. If u7playroom fails, the
  entire homelab is partitioned. Consider direct dorkcore→gateway uplink.

[MEDIUM] INVENTORY-GAP
  CPU model unknown for node1, node2, node3. Will be filled by next
  credentialed discovery run; flagged for visibility.

[MEDIUM] DISCOVERY-AGENTLESS-NEEDED
  Devices marked discovery_source=manual (Mikrotik PTP radios x2) have
  no live verification path. Recommend periodic operator review or SNMP
  read-only credentials if available.

[LOW] DRIFT-CANDIDATE
  GitOps caveat noted: node labels, CoreDNS/authentik-outpost patches,
  and e1000e systemd units are deliberately not in ArgoCD repo.
  Recommend: capture as ConfigurationAssertions so they're verified
  even though they're not Git-managed.

[LOW] PROVENANCE-PARTIAL
  PhysicalPart placements reconstructed from operator notes:
    - node1.dimm_a/b: from node4 (verified by operator)
    - node2.dimm_a:   from node5 (verified by operator)
    - nas0.md0.member[0..2]: from cp1/2/3 (verified by operator)
  Earlier history unknown. Treat pre-current placement as low-confidence.

That's eight findings without an LLM in the loop. Add the LLM and you get readable explanations and "do you want me to file these as tickets / open PRs / write a runbook entry" follow-ups.


Known unknowns to resolve before completing the model

Things the user-supplied data didn't pin down. Each gets filled by either discovery or a quick operator answer:

  1. node1/2/3 CPU models — first SSH probe resolves.
  2. node0 e1000e workaround status — first SSH probe resolves; this is a HIGH finding until verified.
  3. WAN2 outage status — operator-only: is it being fixed, or should the harness recommend decommissioning?
  4. cp1/2/3 replacement USB SSD models/capacities — discovery fills.
  5. Specific AP models (U6 Pro / U7 Pro / mesh variants) — UniFi adapter fills.
  6. Mikrotik PTP radio model + firmware — operator entry; no live source.
  7. Are cp1/2/3 Pi 4B 4GB or 8GB? — listed as 4GB but worth confirming via probe.
  8. Proxmox VM list beyond PBS — Proxmox API fills.
  9. Other ConfigurationAssertions in your head — the 11 in §5 of the inventory are an excellent start, but there are almost certainly more captured only in shell history. A "tell me about this host" conversational extraction agent is one of the highest-value pieces of the eventual UI.

Decision points this walkthrough surfaces

Worth deciding before going further:

  1. Use NetBox in-the-loop, or just use NetBox's model? The walkthrough assumes you actually run NetBox and the harness reads/writes via API. The alternative — model your own SQLAlchemy schema closely inspired by NetBox's, skip the install — is defensible if you'd rather not maintain another Postgres/Redis/worker stack. The custom fields and bend-cases are roughly the same either way; what changes is who owns the canonical store.

  2. Cables: which ones? "Just the chokepoint" is one extreme. "Every cable" is the other. Middle ground: model cables that participate in failure-domain queries (uplinks between switches and to gateways/APs, NICs to switches for hosts whose workloads have HA requirements). Skip patch cables to APs and end-user devices.

  3. K8s pod-level modeling: yes or no? The walkthrough says no — pods are too ephemeral, K8s API is SoT. But a logical service layer in NetBox that the K8s adapter keeps in sync gives you a unified Services view. The question is whether you want NetBox to be that surface, or whether the harness's UI is the right place.

  4. Plugin objects: as NetBox plugins or as harness DB? PhysicalPart/Placement could be a NetBox plugin (writes via NetBox API, follows NetBox's permission model). Cleaner. But it ties you to NetBox for those objects. Harness DB keeps optionality.

Each of those is a real fork in the road. Worth picking before any code gets written.