Apps that serve traffic declare endpoints, and ORC8R gives every endpoint a name. Which names exist, who can resolve them, and who can reach them is decided by one setting per endpoint: its exposure scope. This page is the map — the vocabulary, the three scopes, and how a packet gets from a client to a node. The rest of the section is the how-to.
The vocabulary
Three of the words below are ORC8R's own, and the rest of the section is built out of them.
- An endpoint is one thing an app serves: a port, the protocol spoken there, and an optional readiness probe. Endpoints are declared in the app package — see Authoring apps.
- A slot is a node's stable identity inside its pool — the position that survives when the node behind it is replaced.
- An endpoint's exposure scope —
project,orgorpublic— decides who can resolve it and who can reach it.
One example runs through this whole section. An organization called acme has a project shop with two pools: db, running postgres, and storefront, running a web app. A pool is a group of identical nodes under one name (see Pools), and every node in a pool runs the same apps.
Names, not addresses
Nothing in ORC8R asks you to write down a node's address. Every pool endpoint is reachable by a name, and that name keeps meaning the same thing while the machines behind it come and go:
The pool db in project shop answers to db.shop.internal, and its A/AAAA answers list every node that is online. Replace a machine and the next answer lists the replacement instead — nothing you wrote down goes stale, because you never wrote an address down.
Nodes also get shop.internal as a DNS search domain, so a node in project shop can just say psql -h db.
Names are answered by a resolver inside the agent on each node, from state the server pushes to it. Two things follow: resolution keeps working while the server is unreachable, and answers are filtered by liveness.
The default is isolation
A project is a network boundary, not only an organizational one. When its nodes run on a provider that isolates networking, they share one private network — one segment per project, per zone — and by default a node in project shop cannot reach a node in any other project, while nothing outside shop can reach in at all. You do not switch this on: whether nodes are isolated follows from the provider the operator chose, and the private network is created the first time you deploy on such a provider and torn down when the last of those pools goes away.
Inside that network every node gets a private address the server assigns — the 10.42.7.14 a name resolves to — and it keeps that address as the pool changes around it. Nodes are never renumbered underneath their names.
What the boundary permits is asymmetric. A project's nodes can open outbound connections, but nothing outside the project can open one inbound — until you expose an endpoint. Exposure is exactly how you relax the default, one endpoint at a time: each of the three scopes below opens a controlled, per-endpoint hole in the wall, to the rest of your organization or to the whole internet, and nothing wider than the hole you opened.
These guarantees come from the provider, so they are the provider's to make. Some providers isolate every project on its own private network as described above; others do not isolate at all, or segregate projects by some other means. Self-hosted nodes are the clearest example of the hands-off case: that provider does nothing special with networking, on the understanding that you — the operator who enrolled the machines — have put whatever network controls you need around them. When a provider does not isolate, names still resolve, but whether one node can reach another is up to the network those nodes already sit on. See Providers.
Who can reach it: the three scopes
The exposure scope has three values, and they nest — each opens the wall a little wider than the one before, admitting everyone the previous scope did and more.
| Scope | The name | Who can resolve it |
|---|---|---|
| Project (the default) | db.shop.internal | nodes in the same project |
| Organization | db.shop.internal — the same name | nodes in any project of the same organization |
| Public | storefront.acme.example.com | anyone on the internet |
Two of the three cost an operator nothing. Project scope is on for every pool, always. Org scope needs the setting and nothing else — the routes and the firewall permits between the projects are arranged for you. Public scope has real prerequisites: a base domain, a way to publish DNS records, and a block of public addresses routed to your hosts. Public exposure is the checklist, and Front-door patterns covers the wiring.
Scope is set per endpoint, not per app: the db pool can share its postgres port with the whole organization while its metrics port stays inside project shop.
The package says what, the request says who
An app package declares what it serves — a name, a port, a protocol, an optional readiness check, and how many nodes of a scaled pool may answer for it. It never says who may reach it. That split is deliberate: the same postgres package is a private database in one project and a publicly addressable service in another, and neither fact belongs in the package.
Exposure is decided where the app is assigned to a pool. In the request composer, each app you add has an Exposure section with one row per endpoint, and the row's first control is the scope. See Authoring apps for the package side and Project and organization exposure for the deployment side.
Plain and slot: two ways to mean the pool
Every endpoint of every pool answers two selectors, always. You choose the semantics by choosing the name:
- Plain —
db.shop.internalanswers the pool as a whole. - Slot —
db-1.shop.internalanswers exactly the node holding runtime slot 1, and nothing else. If that node is unhealthy the name answers nothing; it never substitutes a stand-in.
Slots are handed out lowest-first, never move between live nodes, and a replacement node inherits the slot of the node it replaced. So the convention everybody uses is slot 1 is the writer. An app finds out which slot it is running on from the pool.slot parameter and configures itself accordingly.
What the plain name answers
That depends on what the app said. Each endpoint declares a serve mode — primary or spread — in its package (Authoring apps), and the pool's plain names follow the strictest one:
- If any endpoint of the version serves
primary— which is the default — the plain names answer the slot-1 node alone, and answer nothing at all while that node is unready. A name is one answer for every port behind it, and only slot 1 is correct for all of them. - If every endpoint serves
spread, the plain names answer every online node, rotated. Use that for anything stateless, and for read traffic.
The rule covers the pool's derived name, its service-name aliases, and its public name — the same answer under every horizon. The slot names are never touched by it, and neither are SRV records: both already name individual nodes, which is what the rule is reaching for.
Where TLS comes from
Every scope hands your app its own key material through parameters. Nothing terminates TLS on your behalf.
| Scope | Certificate | Who terminates |
|---|---|---|
| project | a leaf from your organization's internal CA, delivered by the tls.cert and tls.key parameters; clients validate it through ca.bundle | your app |
| org | the same leaf and the same bundle — cross-project clients validate through the organization root | your app |
| public | the organization wildcard *.acme.example.com, obtained automatically over ACME and delivered through the same tls.cert and tls.key parameters | your app |
The delivery path
Inside a project or an organization the path is as short as it looks: the resolver hands out node addresses and the client dials one. Nothing sits in between.
Public delivery adds exactly one hop inbound and none outbound. A public name resolves to a single pool address rather than to node addresses. One host at a time holds that address — it holds the claim — and forwards each flow to a node without rewriting it; the node replies out of its own uplink, sourced from the pool address, bypassing the claim host entirely. That shape is called direct server return. Three consequences follow:
- Ingress funnels through one host per pool address; egress — the heavy direction — leaves from whichever node served the flow.
- When the claiming host fails, the control plane reassigns the claim within seconds, without touching DNS. Clients reconnect to the same name and port.
- Your app sees the real client address, not a proxy's.
Since the name can no longer pick the node, the port does. Each exposed port forwards to that endpoint's serving set, decided by the endpoint's own serve mode:
primary— the slot-1 node alone. While it is unready the port refuses connections rather than serving them from a stand-in.spread— every node currently passing that endpoint's readiness probe, spread by a consistent hash of the flow, so a connection stays on the node it started on. It refuses only when no node passes.
Both halves are the app's own statement: the mode comes off its package and the health comes off the probe it declares. There is no deployment-side knob for either. Because a primary port lands all its traffic on one node, the claim is placed on that node's host wherever it can be — the write path then takes no extra hop at all.
Public exposure follows a request through this path step by step.
Related pages
- Authoring apps — the app-author side: ports, readiness checks, and the values ORC8R fills in.
- Project and organization exposure — using these names from your nodes, and sharing a service across projects.
- Public exposure — domains, addresses, and certificates.
- Front-door patterns — how a block of public addresses reaches your machines.
- Troubleshooting networking — when a name or a port does not answer.