drift Docs
Start
What is Drift?
The tour, if you are new here.
Use cases
Whether Drift does your thing.
Getting started
Nothing to deployed, in one command.
Architecture
How a slice is put together.
What it costs
The free grant, four unit prices, two rules.
Build
Canvas
Static sites, same origin as your API.
Tools
Operate
Auth
Route gates, API keys and your account.
Security
Boundaries, sandboxing and hardening.
Troubleshooting
Error codes
What went wrong, and what to do about it.
Legal
Acceptable use
What a slice may not be used for.
Data processing
The DPA, and every sub-processor.

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.

Driftfile
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")
    }
  }
}
TypeDefaultWhat drift apply does with it
  • rate_limit
    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.
  • egress
    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.
  • domain "host"
    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.
  • site_auth
    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.
  • links
    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.
  • allow
    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.
  • hosts
    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  added

egress: what a function may dial

Driftfile
edge {
  egress {
    mode  = "allowlist"
    hosts = ["api.stripe.com", "smtp.sendgrid.net:587"]
  }
}
TypeDefaultMeaning
  • mode
    Type
    "open" or "allowlist"
    Default
    "open"
    Meaning
    "open" reaches any public host. "allowlist" reaches only hosts.
  • hosts
    Type
    list of string
    Default
    none
    Meaning
    host or host:port: api.stripe.com is every port on that host, smtp.sendgrid.net:587 only 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

Driftfile
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

Driftfile
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 place

A 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

Driftfile
edge {
  site_auth {
    realm = "Staging"

    user "alice" {
      password = env("ALICE_PASS")
    }

    user "bob" {
      password = env("BOB_PASS")
    }
  }
}
TypeDefaultMeaning
  • realm
    Type
    string
    Default
    none
    Meaning
    The name a browser shows in its password prompt. At most 64 characters.
  • user "name"
    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.
  • password
    Type
    env("NAME")
    Default
    REQUIRED
    Meaning
    Inside a user: the name of an environment variable holding the password. Written env("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 file

Each 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.

Driftfile
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

Driftfile
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

Driftfile
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 with drift 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:

Driftfile
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.