Edge: what reaches the slice, and what it reaches
edge is the one block where a Driftfile says how a slice meets the network, in both directions. Inbound: the hostnames it answers on, a password gate in front of its site, its request rate, and which addresses may reach it at all. Outbound: the hosts its Pulses may dial, the other slices of yours it may call, and the hostnames it serves on their behalf. It sits at the top level of the file, beside element, the Backbone blocks and site. A file has at most one.
This page is the reference for each attribute and block. How each rule behaves on a live slice, what a refused request looks like and the commands that go with it are in the Edge section.
edge {
rate_limit = "600/min"
links = ["billing", "admin"]
allow = ["203.0.113.0/24", "198.51.100.7/32", "2001:db8::/32"]
hosts = { "admin.example.com" = "admin" }
egress {
mode = "allowlist"
hosts = ["api.stripe.com", "smtp.sendgrid.net:587"]
}
domain "shop.example.com" {}
domain "admin.example.com" {}
site_auth {
realm = "Staging"
user "alice" {
password = env("ALICE_PASS")
}
user "bob" {
password = env("BOB_PASS")
}
}
}- Type
"N/s","N/min"or"N/h"- Default
- the slice's
- What drift apply does with it
- Sets the slice's requests per minute. The same limit as
limits.requests_per_minute.
- Type
- block
- Default
- the slice's
- What drift apply does with it
- Sets the slice's outbound posture:
"open", or"allowlist"with the hosts it may reach.
- Type
- block, one per host
- Default
- none
- What drift apply does with it
- Attaches every listed hostname the slice does not have yet. Never removes one.
- Type
- block
- Default
- the slice's
- What drift apply does with it
- Sets the site gate to exactly these users, with passwords read from the environment.
- Type
- list of string
- Default
- the slice's
- What drift apply does with it
- Makes the slice's links exactly this list: adds the missing ones, removes the rest.
- Type
- list of string
- Default
- the slice's
- What drift apply does with it
- Replaces the inbound allowlist with these CIDRs. An empty list removes it.
- Type
- map of string
- Default
- the slice's
- What drift apply does with it
- Replaces the hosts this slice fronts for linked slices. An empty map removes them.
A key you leave out is left alone
Every attribute and block is optional, and one that is left out leaves that part of the slice exactly as it is. A file with no site_auth does not switch a gate off, a file with no links does not unlink anything, and a file with no rate_limit keeps the rate the slice already has. So you can adopt edge one part at a time, and a project that shares a slice with another can leave a part to whichever project owns it.
A part that is written is the whole truth for it. links = ["billing"] removes a link to admin; a site_auth naming one user shuts out every other; allow = [] opens the slice to every address again. What the slice has and the file does not is a removal, and drift plan lists it under Removes, where drift apply asks about it. domain is the one written part that only adds: it attaches what is missing and never detaches a hostname.
egress follows the same rule as the rest: with no egress block at all, the slice keeps the posture it has, so a slice an earlier apply put on an allowlist stays on it until a file declares mode = "open". An egress block with no mode is "open".
Everything except egress and domain is checked before anything is deployed. A rate that cannot be held, a password variable that is not set, a link to the slice itself, an entry in allow that is not a CIDR, or a hosts entry missing either half refuses the whole file, so nothing half-applies. drift plan reads the live rate, gate, links, allowlist and hosts and shows each beside the declared one, with + for what would be added and - for what would be removed, and changes nothing. Each part has its own address in the plan:
~ limits.requests_per_minute 60 → 600
~ egress open → allowlist api.stripe.com, smtp.sendgrid.net:587
+ site_auth the gate is turned on (realm Staging)
+ site_auth alice password from $ALICE_PASS
+ link billing added
- link docs removed
+ allow 203.0.113.0/24 added
+ domain shop.example.com added
+ host admin.example.com → admin addedegress: what a function may dial
edge {
egress {
mode = "allowlist"
hosts = ["api.stripe.com", "smtp.sendgrid.net:587"]
}
}- Type
"open"or"allowlist"- Default
"open"- Meaning
"open"reaches any public host."allowlist"reaches onlyhosts.
- Type
- list of string
- Default
- none
- Meaning
hostorhost:port:api.stripe.comis every port on that host,smtp.sendgrid.net:587only that port. Concrete hostnames only: a wildcard cannot be resolved to an address, so it is refused.
The apply resolves every host to its current addresses and renders them into the slice's own outbound rule, so a call to a host that is not listed is refused at the network. An allowlist with no hosts denies every outbound request. Private ranges (RFC-1918, link-local and CGNAT) are blocked in both modes. The Driftfile is the source of truth for the list: drift atomic egress list, refresh and test inspect what the slice currently has, and there is no command that adds a single host. Edit hosts and apply again. Egress covers the details, including what a refused call looks like.
domains: custom hostnames
edge {
domain "forms.gemeente.example" {}
domain "tenants.example.org" {
wildcard = true
}
}Each domain "host" block is a hostname the slice answers on beyond its default <user>-<slice>.ondrift.eu. The apply attaches the ones the slice does not have. You then point DNS at the slice and prove the domain with a TXT record: drift slice domain verify <host> checks it and issues the certificate, and the domain routes only once it is proved. A hostname live on the slice and missing from the file stays attached, and the apply reports it. Domains has the attribute, and Hostnames and domains the rest of the flow. A free slice grants no custom domains.
rate_limit: requests per minute
edge {
rate_limit = "600/min"
}The most requests the whole slice accepts, counted across every request that reaches it. Past it, a client gets 429. Write it per second, per minute or per hour; the slice holds a whole number of requests per minute, so the CLI converts it:
| Written | Held as |
|---|---|
"10/s" | 600 per minute |
"600/min" | 600 per minute |
"3600/h" | 60 per minute |
"30/h" | refused: below one per minute. The smallest you can write is "1/min" (or "60/h"). |
"90/h" | refused: 1.5 per minute is not a whole number. Write a multiple of "60/h". |
Driftfile:6:16: Not a rate: edge.rate_limit: "90/h" is 1.5 requests per minute, and a slice holds a whole number per minute; write a multiple of 60/h, such as "60/h" or "120/h"edge.rate_limit and limits.requests_per_minute are one limit. Write it in one place: a file that writes both is refused, even when the two agree.
Driftfile:10:16: Written twice: edge.rate_limit and limits.requests_per_minute are one limit; write it in one placeA free slice is held to 60 requests per minute, the figure it is granted. Declaring more is refused with a message naming that ceiling; a monthly slice has no ceiling here.
Driftfile:2:13: Not the free slice: billing is "free", and limits.requests_per_minute is 600, and the free slice grants 60. Make the shape smaller, or write billing = "monthly"A rate left out takes the platform's figure for the slice. Rate limiting covers how requests are counted.
site_auth: a password in front of the site
edge {
site_auth {
realm = "Staging"
user "alice" {
password = env("ALICE_PASS")
}
user "bob" {
password = env("BOB_PASS")
}
}
}- Type
- string
- Default
- none
- Meaning
- The name a browser shows in its password prompt. At most 64 characters.
- Type
- block, one per user
- Default
- REQUIRED
- Meaning
- At least one. A gate nobody can pass is refused. The label is the username a visitor types: 1 to 64 characters, no colon or whitespace. A user named twice is refused.
- Type
env("NAME")- Default
- REQUIRED
- Meaning
- Inside a
user: the name of an environment variable holding the password. Writtenenv("NAME")and nothing else.
The gate is HTTP Basic auth on the slice's Canvas paths only. /api and /trigger carry their own credentials and are never asked for this one. A visitor without the credential gets 401 and never reaches the site.
A password is never written in the file. The Driftfile is committed, so a literal there would be committed too, and it is refused. The refusal never prints the value:
Driftfile:9:18: A secret is a reference: site_auth user "alice": password is write-only and must be written env("NAME"): a literal here would be a credential committed with this fileEach password names a variable that the apply reads, through the same env() sources as a secret: your shell's environment first, then --var NAME=value, then .env.<environment> and .env beside the Driftfile. A variable that is unset or empty refuses the file before anything deploys, naming the variable and the user; every such variable is reported at once. No password value is ever printed. The plan lists each user as site_auth alice, and a password is write-only, so it is set again from its variable on every apply rather than compared.
Written, the block replaces the whole credential set, so a user it no longer names is shut out, and the plan lists them under Removes. Left out, the gate is left as it is, on or off. No declaration turns the gate off: delete the block and run drift slice auth disable, which confirms first because it is the one direction that exposes the site. The site gate covers what a visitor sees and how the slice's Deed checks each password.
links: your other slices this one may call
edge {
links = ["billing", "docs"]
}Each name is another slice you own. A linked slice is what a Pulse calls with drift.Edge.Slice("billing"), and what hosts may hand a hostname to.
When links is written it is the whole list. The apply adds every link the slice does not have and removes every live link the list does not name, adds first so a link you keep is never down between the two. links = [] removes them all. Leave it out and the links stay as they are. The plan lists each as link billing. A slice cannot link to itself, and a name that is not a slice of yours is refused by the platform with its reason.
A Pulse never learns where a linked slice lives. It calls a local address on its own slice, and the slice runtime relays the call to the linked slice inside the cluster, so the call never crosses the public internet and no cluster name reaches your code. A call to a name the slice is not linked to fails before any connection is made:
slice "billing" is not linked: declare it under edge.links in the Driftfile (or run drift slice link add billing)Links covers the relay, the commands and what a linked slice can and cannot tell about its caller.
allow: which addresses may reach the slice
edge {
allow = [
"203.0.113.0/24",
"198.51.100.7/32",
"2001:db8::/32",
]
}When the list names any range, only clients inside one of them reach the slice. Everyone else gets 403, never a 404: the host exists, and the client is being told no.
HTTP/1.1 403 Forbidden
Content-Type: application/json
X-Drift-Refused-By: edge.allow
{"error":"this site does not accept requests from your network address","code":"EDGE-ALLOW"}The header is there for a person reading curl -i, the code for a client branching on the body. The check runs before anything else is spent on the request, ahead of the rate limit and the site gate, so a refused client reaches neither Canvas nor Atomic and uses none of the rate the admitted addresses share.
Every entry is a CIDR range. A bare address is refused, and the message gives the range that admits that one address (203.0.113.7/32, or /128 for IPv6). An IPv4 client arriving in IPv6-mapped form is compared as the IPv4 address it is. A slice may list at most 256 ranges. allow = [] removes the list and every address reaches the slice again; leaving it out keeps the list the slice has. The plan lists each range as allow 203.0.113.0/24. See The IP allowlist.
hosts: fronting a linked slice
edge {
domain "admin.example.com" {}
links = ["admin"]
hosts = { "admin.example.com" = "admin" }
}hosts is a map from a hostname to the linked slice that serves it. A request for the host is served by that slice. That lets one slice own the hostnames for a whole set of yours, with each hostname answered by the slice that does the work.
Two conditions, both checked when the apply sends the map:
- The host is a verified custom domain of this slice. Declare it as a
domain, verify it withdrift slice domain verify <host>, then name it here. - The slice is linked from this slice. Declare it under
links; the apply sets links before hosts, so one file can do both.
A refusal names the command that fixes it.
A fronted host is served with the target slice's rules, all of them. Its rate limit, its site gate and its address allowlist apply, not this slice's, so fronting can never reach a slice around the rules it declares for itself. The link stays the authority: it is checked again on every lookup, and once it is removed the host stops routing. hosts = {} stops fronting every host. The plan lists each as host admin.example.com → admin. See Fronting a linked slice for the refusals.
Per environment
An environment is another slice from the same file, and it changes only what a variable carries, plus its slice and billing. To give one environment a different edge setting, write the setting as a variable:
variable "rate" {
default = "600/min"
}
environment "staging" {
vars = { rate = "60/min" }
}
edge {
rate_limit = var.rate
links = ["billing"]
}drift plan and drift apply read 600/min for the slice itself and 60/min for staging; links is the same in both.
What edge does not hold
Content-Security-Policy is not here. It is a property of a document that a browser enforces, and it stays with Canvas; the rules in edge are the ones Drift enforces itself. Pulse gates are not here either: an auth block on a Pulse guards that Pulse, and site_auth only ever guards Canvas paths. See Authentication.