Getting started
We'll build a small guestbook: a page with a form, an API that stores and lists the messages, and a database behind it. Along the way you'll touch all three Drift services: Atomic for the API, Backbone for the data, and Canvas for the site, and never leave your terminal.
Everything you deploy lands in a slice: your project's own private corner of Drift, with its own functions, data, site and URL, isolated from everyone else's. The deploy in step 7 creates one for you.
1. Install the CLI
Everything on Drift happens through the drift command-line tool. Every release publishes
two install paths. Homebrew is the shortest, because it fetches a prebuilt binary, so there's no
toolchain to install and nothing to compile:
Already have Go? This builds the same CLI from source:
Either way, confirm it's there. Two numbers come back and they move independently: the version of this binary, and the version of the Driftfile schema it implements.
2. Get an account
Drift is in closed alpha, so signing up needs an invite code. A code is minted for one specific email address and works once. Redeem it and choose your own username and password:
The CLI asks for a username (2–32 lowercase letters and digits, with no hyphens or underscores), your email address, and a password of at least eight characters. The email has to be the one the code was issued for; anything else is refused. The invite code stands in for the emailed verification code, so there's no second round trip to wait for.
In CI, keep the password out of shell history.
ps:
echo "$PASS" | drift account create -u alice -e alice@example.com --password-stdin --invite-code <code>
Creating the account logs you in. On another machine, sign in with the credentials you chose:
The session lands in ~/.drift/session.json, bound to a per-machine device ID, so this is
a one-time step on each machine.
3. Lay out the project
A Drift project is folders and a Driftfile. Atomic
functions are flat source files directly under atomic/, with no per-function folders and no
list to maintain; your site lives under canvas/. Here's the shape we're about to build:
-
guestbook/the project root
-
atomic/every function, discovered flat
- sign.goPOST, save a message
- entries.goGET, list messages
-
canvas/the site, served at /
- index.htmlthe page visitors see
- Driftfilewhat the slice is made of
-
atomic/every function, discovered flat
4. Write the API
An Atomic function is a normal source file with a one-line directive above the
function it decorates. The directive, not a config file, declares how the function is
reached: the HTTP method, the route, and whether it needs authentication. Here's the endpoint that
saves a message, in Go (atomic/sign.go):
package main
import drift "github.com/ondrift/cloud/sdk/go"
// @atomic http=post:sign auth=none
func PostSign(body map[string]any, _ drift.Request) (int, string, any, map[string]string) {
id, _ := drift.Backbone.NoSQL.Collection("guestbook").Insert(body)
return 201, "Created", map[string]any{"id": id}, nil
}And the one that lists messages back (atomic/entries.go):
package main
import drift "github.com/ondrift/cloud/sdk/go"
// @atomic http=get:entries auth=none
func GetEntries(_ drift.Request) (int, string, any, map[string]string) {
all, _ := drift.Backbone.NoSQL.Collection("guestbook").List(nil)
return 200, "OK", all, nil
}
Every function returns the HTTP status, a short status message, and the response body (the SDK
serialises objects to JSON for you). Go adds a fourth value, a response-headers map,
nil when you don't need one; Python, Node, Ruby, PHP and Rust return the three values
only. The directive does the routing, where http=post:sign means "POST, reachable at
sign", so there's no router to wire up.
The directive must sit directly above a named function.
pub fn in Rust, a named function rather than an arrow in
Node. Blank lines in between are fine, other code is not. An @atomic line floating at
the top of the file above the imports decorates nothing, and the CLI refuses the deploy rather than
silently shipping zero functions.
There is no database to set up.
drift.Backbone.NoSQL.Collection("guestbook") is the data layer, and the SDK talks to
Backbone for you, and the collection springs into existence the
first time you write to it.
5. Add the page
Canvas hosts your static site. Its best trick: a Canvas page can call its own functions on the
same origin, so the browser just fetches /api/sign and
/api/entries, with no CORS to configure and no separate API domain. Save this as
canvas/index.html:
<h1>Guestbook</h1>
<form id="form">
<input name="name" placeholder="Your name" required>
<input name="message" placeholder="Your message" required>
<button>Sign</button>
</form>
<ul id="entries"></ul>
<script>
fetch("/api/entries").then(r => r.json()).then(rows => {
rows.forEach(e => entries.innerHTML += `<li><b>${e.name}</b>: ${e.message}</li>`);
});
form.onsubmit = async (ev) => {
ev.preventDefault();
await fetch("/api/sign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(Object.fromEntries(new FormData(form)))
});
location.reload();
};
</script>6. Declare it in a Driftfile
The Driftfile ties the project together: the resources your
slice needs and how big they are, all at the top level (the file is the project). Save
this as Driftfile at the project root:
name: guestbook
backbone:
nosql:
- { name: guestbook, size: 50MB }
canvas:
sites:
- ./canvas
The functions aren't listed. With flat files under atomic/ the CLI discovers every
@atomic function for you, so there's no list to keep in sync. You only reach for
atomic.functions to pin the folder-per-function layout or to attach per-function options.
NoSQL collections, SQL databases and blob buckets each need their own size.
backbone.queue_max_depth), cache entries take a file or
value plus a TTL, and secrets are a plain KEY: value map.
7. Deploy everything
One command reads the Driftfile, creates the slice, provisions the collection, compiles and ships both functions, and publishes the site:
Before it changes anything, the CLI prints exactly what it will create and what that costs, then waits
for a y:
Slice "guestbook" does not exist. Will create:
atomic.functions: 2
backbone.nosql_collections: 1
backbone.nosql.guestbook: 50MB
Atomic functions 2 x €0.05 = €0.10
Function memory (MiB) 32 x €0.03 = €0.96
Storage (per GiB) 0.0488 x €0.25 = €0.01
Cost: €1.07/month
Apply? [y/N]
The memory line appears even though the Driftfile never mentions memory.
atomic.function_memory is optional, but a slice that runs functions can't run them in zero
memory, so an omitted value resolves to 32MB, and the resolved shape is what gets priced.
Declare it to buy more; anything from 32MB to 256MB is accepted, at
€0.03 per MiB per month. See what it costs for the full
price list and the free grant.
Deploys only ever create or grow a slice. A Driftfile that declares less than the
live slice is refused rather than applied, because shrinking deletes data the manifest can't know
about. To apply a shrink deliberately, use
drift slice resize --from Driftfile --allow-destructive. To see the plan without
deploying anything, add --plan.
8. See it live
Your slice answers at <username>-guestbook.ondrift.eu, with TLS handled for you. Open
that URL to use the guestbook in the browser, or hit the API directly:
# sign the guestbook
curl -X POST https://<username>-guestbook.ondrift.eu/api/sign \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "message": "Hello from Drift!"}'
# read it back
curl https://<username>-guestbook.ondrift.eu/api/entries9. Inspect it from the terminal
You can read and poke at your slice's data without leaving the shell:
# list what's in the guestbook collection
drift backbone nosql list --collection guestbook
# store a secret (read fresh on every invocation of a function that declares it)
drift backbone secret set API_KEY=sk-12345
# push a message onto a queue
drift backbone queue push notify '{"to": "alice", "text": "welcome!"}'
# cache a value for an hour
drift backbone cache set greeting "hello world" --ttl 3600
# check the slice's health and usage
drift backbone statusA function only receives the secrets it names.
// @atomic http=post:sign auth=none secrets=API_KEY. The runner fetches each named
secret on every call and passes it in as DRIFT_SECRET_API_KEY, so changing a
value takes effect on the next request with no redeploy. Adding a name to the
secrets= list is the part that needs one.
Bare drift opens the same data in a full-screen terminal dashboard, with slices down
the side, and Atomic / Backbone / Canvas panes for browsing functions, documents and sites.
Shaping the slice yourself
Step 7 created the slice as a side effect of deploying. To create one up front instead, pick the path that fits:
| Command | What it does |
|---|---|
drift slice create <name> | Opens the create form in the terminal dashboard: every dial, with the price recomputed as you change a value. |
drift slice create <name> --free | Takes the fixed free Hacker preset with no form, so it works in CI and over SSH. |
drift slice create --from Driftfile | Born at the shape the manifest declares, the same shape a first project deploy would have produced. Add -y to skip the cost prompt. |
All three set the new slice as the active one, so the commands above know where to go. Switch between
slices later with drift slice use <name>.
The free Hacker preset is fixed.
size still grows the slice, because the grant hands out
collection slots but no per-collection storage quota. What decides whether it stays free is the price of
the resulting shape, and the prompt shows that as free → €N/mo before anything happens.
Write in your language
Functions can be written in Go, Python, Node.js, Ruby, PHP, or Rust, and the CLI detects the language from your source files. The same "sign" endpoint looks like this in Python…
import drift
# @atomic http=post:sign auth=none
def post_sign(body, req):
entry_id = drift.backbone.nosql.collection("guestbook").insert(body)
return 201, "Created", {"id": entry_id}…and in Node.js:
const drift = require("@ondrift/sdk");
// @atomic http=post:sign auth=none
async function postSign(body, req) {
const id = await drift.backbone.nosql.collection("guestbook").insert(body);
return [201, "Created", { id }];
}
module.exports = { postSign };
drift project deploy handles all of them the same way: it walks atomic/,
finds every decorated function whatever the language, and ships them together.
drift atomic deploy <folder> is the single-function path instead. It takes one
folder and deploys the first @atomic annotation it finds in it, so point it at a folder
holding one function, not at a folder holding several.
Rust takes one function per folder.
Take your data and go
Drift has no lock-in. At any point you can snapshot a slice and download the whole thing: source code, database contents, secrets, and static sites, with no Drift-specific files in the archive:
drift slice snapshot create --name my-backup
drift slice snapshot download <snapshot-id>Optional: use your own domain
Your slice answers at <username>-guestbook.ondrift.eu, and you can point your own
hostname at it, and Drift verifies ownership and issues the TLS certificate for you. Add a domain
to your active slice:
The CLI prints two DNS records to create at your registrar:
# 1. prove you own the domain
_drift-challenge.guestbook.example.com. TXT "drift-verify=<token>"
# 2. route the hostname to your slice
# (or an A record to the platform's IP at a zone apex, where CNAMEs aren't allowed)
guestbook.example.com. CNAME ingress.ondrift.eu.Once DNS has propagated, verify. Drift checks the TXT record and starts issuing the certificate:
Watch it go live (and see every domain on the slice) with:
When the status reads live, your guestbook is served on your own domain, HTTPS and all.