drift Docs
Start
What is Drift?
The tour, if you are new here.
Why Drift?
The case for a smaller cloud.
Getting started
Nothing to deployed, in one command.
Architecture
How a slice is put together.
What it costs
The free grant, five unit prices, two rules.
Build
Canvas
Static sites, same origin as your API.
Tools
Operate
Auth
Accounts, tokens and scopes.
Security
Boundaries, sandboxing and hardening.

Canvas

Canvas serves your static files (HTML, CSS, JavaScript, images) and forwards any request under /api/* to your Atomic functions. Your frontend and your backend live at the same origin, so the browser calls /api/… with no CORS headers, no proxy config, and no separate API domain to manage.

There is no CORS to configure.

A React (or Vue, or plain-HTML) app deployed on Canvas fetches /api/items the way it would fetch a local file: same host, same TLS, one deploy. There is no CORS preflight to configure because there is no second origin.

Deploy a site

Shell
# serve a folder at the root
drift canvas deploy ./my-site

# mount a folder under a sub-path
drift canvas deploy ./admin --route /admin

Canvas zips the folder, uploads it, and serves it over HTTPS at your slice's URL. Re-deploying the same route replaces that site's files wholesale. deploy is the only drift canvas subcommand; sites go away by pruning, not by a delete command.

How routing works

One public listener answers every request to your slice, and it checks a short list of reserved prefixes before it looks for a file. Order matters:

Request Handled by
GET /realtime/<channel>Backbone Realtime: the WebSocket upgrade, matched first
GET /api/itemsAtomic → the http=get:items function
POST /api/itemsAtomic → the http=post:items function
/trigger/…Atomic → the trigger surface
GET /health, GET /readyThe slice's own liveness probes
/deploy, /prune, /admin/sitesCanvas's operator surface: what the CLI's deploy path calls
GET /Canvas → index.html
GET /styles.cssCanvas → the static file

Everything that is not one of those prefixes is served as a static file. The reserved names belong to the platform, so a site mounted at / cannot serve its own /health or /deploy, and a wrong method on /deploy or /prune answers 405 rather than falling through to your files. A site mounted at a sub-route is unaffected: /blog/health is an ordinary file.

The /realtime/ intercept is why a browser subscribes at wss://<your-slice>/realtime/<channel>, and that URL is a Canvas route handed to Backbone's hub. See Backbone for the channel protocol.

Response headers

Every static response carries a fixed header set. There is no flag, Driftfile key or API to change any of it:

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()

The CSP confines every subresource to your own origin.

A CDN <script>, a Google Fonts stylesheet (and its font files, which fall back to default-src 'self'), a remote image, and any fetch, XHR or WebSocket to another host are blocked by the browser. Vendor what you need into the folder you deploy. X-Frame-Options: DENY also means a Canvas site cannot be embedded in an iframe.

Your own /api/* calls and /realtime/ socket are same-origin, so connect-src 'self' allows them. 'unsafe-inline' is granted to scripts and styles, so an inline <script> or a style attribute works. Content types come from the file extension, and Canvas answers a conditional If-Modified-Since request with 304.

Single-page apps

Deploy your framework's build output, the contents of dist/ or build/, like any other folder. Two rules about how paths resolve decide whether a client-routed app survives a refresh.

There is no history-API fallback. A path with no file extension is treated as a directory and index.html is appended to it, so GET /users/42 resolves to users/42/index.html inside your site. A Vite or CRA build does not contain that file, and the answer is 404. The two ways to keep deep links working:

  • Hash routing, as in /#/users/42. Browsers never send the fragment, so every request is for / and lands on your one index.html.
  • A directory per route: pre-render (static export), so users/42/index.html genuinely exists in the folder you deploy. This only covers routes you can enumerate at build time.

The injected <base href>

When Canvas serves an HTML file that contains a literal lowercase <head> and no <base>, it inserts a <base href> pointing at the directory of the request URL, not at the route the site is mounted on. For a site deployed with --route /blog:

Request Injected
/blog<base href="/">
/blog/<base href="/">
/blog/index.html<base href="/blog/">

A sub-route resolves its assets against the origin root.

A visitor arrives at /blog or /blog/, and both get <base href="/">, so relative asset paths resolve against the origin root and reach the root site or a 404. Build with the base path set to your route (--base=/blog/ in Vite, homepage in CRA) so the emitted URLs are absolute, or write your own <base href="/blog/">, which Canvas leaves alone.

Injection is skipped when the document contains no lowercase <head> token, and when the bytes contain <base anywhere. A site mounted at / gets a base href that matches its own directory, so it needs none of this.

Multiple sites

A slice can host several sites, each at its own route. The longest matching route wins, so a site at / never shadows one at /blog whatever order you deployed them in:

Shell
drift canvas deploy ./marketing             # at /
drift canvas deploy ./blog  --route /blog   # at /blog
drift canvas deploy ./admin --route /admin  # at /admin

Each route becomes a slug that names the site's directory on the slice: / is default, /blog is blog, /admin/portal is admin-portal.

Removing a site

There is no drift canvas delete. Sites are removed by pruning: drift project deploy deploys every site listed under canvas.sites in your Driftfile, then deletes every site the slice is holding that the Driftfile did not name.

A site missing from the Driftfile is destroyed by the next deploy.

One deployed by hand with drift canvas deploy ./admin --route /admin and never added to the Driftfile is destroyed by the next drift project deploy. Declare every site you mean to keep.

Limits

ValuePast it
  • Upload body
    Value
    100 MB
    Past it
    413, nothing is deployed
  • ZIP payload vs. the slice's Canvas size
    Value
    50 MB on the free tier
    Past it
    413, nothing is deployed
  • Files per site
    Value
    10,000
    Past it
    Extraction stops; the deploy reports success

The size check is against the compressed ZIP of the deploy you are making, not the total across every site on the slice. Declare canvas.canvas_size in your Driftfile to size the slice for more.

The file ceiling is the one that fails quietly.

A site over 10,000 files deploys without an error and serves 404 for whatever did not make it in. Count the files in the folder before shipping a large asset tree.

In a Driftfile

Declare your sites alongside everything else and ship them with one command:

Driftfile
canvas:
  canvas_size: 100MB      # optional: total Canvas storage for the slice
  sites:
    - ./site                # mounted at /
    - dir: ./blog
      route: /blog          # mounted at /blog

A full-stack app, meaning a frontend, an API, and its data, is three sections of one file:

Driftfile
name: my-app
backbone:
  nosql:
    - { name: items, size: 50MB }
canvas:
  sites:
    - ./frontend/build

The functions need no listing, because flat files under atomic/ are discovered automatically. Stored Backbone resources carry their own size, which is both the billing driver and the enforced quota, so nosql, blobs and sql entries are refused without one. Queues, cache entries and secrets take no size at all, so a queue map that carries any key besides name is refused.

Your build is served at /, your functions answer at /api/*, and your data lives in Backbone, all on the same origin. See the Driftfile reference for every option.

Custom domains

Every slice answers at <username>-<slice>.ondrift.eu with TLS handled for you, and you can point your own hostname at it:

Shell
drift slice domain add yoursite.com       # prints the DNS records to add
drift slice domain verify yoursite.com   # re-checks the TXT, then issues the certificate
drift slice domain list                  # host, status, wildcard, detail
drift slice domain remove yoursite.com   # also drops its certificate and routing

Getting started walks the full flow.

A subdomain per tenant

--wildcard routes every subdomain of a host to the same slice:

alice.imprente.com, bob.imprente.com and every other subdomain reach the same Canvas site and the same functions. Drift delivers the request; the app decides what the subdomain means. The match strips one label, so a.b.imprente.com is not covered by a wildcard on imprente.com.

This is routing, not a wildcard certificate.

Each subdomain gets its own certificate, issued on first request. And the public hostname does not reach your functions: the frontdoor rewrites Host to the internal backend before proxying. Read location.hostname in the browser and send the tenant explicitly, in the path, the body, or a header of your own.