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.

Languages

The same API in six languages. What changes between them is casing, how the return value is packed, and which dependency manifest the element carries, not what you can call.

Conventions

Handler casingExampleDependencies
  • Go
    Handler casing
    PascalCase
    Example
    PostItems
    Dependencies
    stdlib only
  • Python 3.9+
    Handler casing
    snake_case
    Example
    post_items
    Dependencies
    stdlib only
  • Node.js 18+
    Handler casing
    camelCase
    Example
    postItems
    Dependencies
    built-in APIs only
  • Ruby 3.0+
    Handler casing
    snake_case
    Example
    post_items
    Dependencies
    stdlib only
  • PHP 8.1+
    Handler casing
    snake_case
    Example
    post_items
    Dependencies
    built-in functions only
  • Rust
    Handler casing
    snake_case
    Example
    post_items
    Dependencies
    serde_json + ureq (no TLS by default)

Casing matters only in the one-function-per-folder layout, where the CLI derives the handler name from the route. In the flat layout the name is yours; see Writing a handler.

The same handler, six ways

A GET that reads a value from cache, in every language. The return is (status, message, payload) everywhere; Go adds its headers map.

Go

Go
// @atomic http=get:menu auth=none
func GetMenu(req drift.Request) (int, string, any, map[string]string) {
    menu, _ := drift.Backbone.Cache.Get("menu")
    return 200, "OK", menu, nil
}

Python

Python
# @atomic http=get:menu auth=none
def get_menu(req):
    menu = drift.backbone.cache.get("menu")
    return 200, "OK", menu

Node.js

Node.js
// @atomic http=get:menu auth=none
async function getMenu(req) {
    const menu = await drift.backbone.cache.get("menu");
    return [200, "OK", menu];
}
module.exports = { getMenu };

Ruby

Python
# @atomic http=get:menu auth=none
def get_menu(req)
    menu = Drift::Backbone::Cache.get("menu")
    [200, "OK", menu]
end

PHP

// @atomic http=get:menu auth=none
function get_menu($req) {
    $menu = \Drift\Backbone\Cache::get("menu");
    return [200, "OK", $menu];
}

Rust

Rust
// @atomic http=get:menu auth=none
pub fn get_menu(_req: Value) -> (i64, &'static str, Value) {
    // cache::get returns Option<Value>: a miss is None, not an error
    let menu = drift_sdk::backbone::cache::get("menu").unwrap_or(Value::Null);
    (200, "OK", menu)
}

Packaging & dependencies

The packaging unit is an element: one directory, one language, one dependency manifest, and any number of @atomic functions across flat files inside it. The flat source directly under atomic/ is the default element; a subdirectory of atomic/ holding annotated source is a named one. The CLI scans every file in an element for an @atomic annotation, and files without one are ordinary package code.

  • The version is the CLI's, the manifest is mostly yours. On deploy (and on drift atomic fetch for offline work) the CLI resolves dependencies once for the whole element, referencing the SDK unversioned at its latest tag. For Go it generates the go.mod itself and runs go get github.com/ondrift/cloud/sdk@latest plus go mod tidy; import the SDK as github.com/ondrift/cloud/sdk/go and leave the rest alone. For Python, Node, Ruby and PHP the element's own manifest has to name the SDK, and the deploy checks before it builds and refuses with the line to add, because a manifest that exists but omits it resolves nothing and fails at the first request instead. Don't pin a version yourself in either case.
  • Third-party dependencies work. Because the CLI runs your language's own resolver (go mod tidy, npm, pip, bundler, …), your code is not limited to the standard library: add a normal import and it's fetched at deploy. Use a real password-hashing library, a real validation crate, whatever you need. The zero-dependency rule applies to the SDK itself (our promise to you), not to your functions.
  • Sharing code across functions. Functions in the same element share the directory, so a helper file beside them is ordinary package code they can all call. Across elements there is no implicit sharing, because each is its own package and sibling directories aren't on the import path. For logic two elements need, publish it as a normal module and import it like any other dependency.

Local development

drift atomic run serves your function locally over HTTP, backed by an in-memory Backbone: NoSQL, cache, queues, blobs, locks, and secrets all work with no platform running.

In-memory state is wiped when the process stops, secrets are read from your .env file, and outbound HTTP requests hit real endpoints. Three parts of the surface behave differently:

LocallyWhat to do
  • SQL
    Locally
    Unserved. The zero-dependency SDKs bundle no SQL engine, so drift.Backbone.SQL(…) has nothing to talk to.
    What to do
    Exercise that path against a deployed slice.
  • Deed
    Locally
    Every KeyAuth, JWT, Vault, Link and Pocket call errors: deed requires a running slice (DEED_URL).
    What to do
    Deploy to a slice to test a login flow. The error is deliberate, because a silent no-op would report success while storing nothing.
  • Realtime
    Locally
    Callable but inert. Publish and Presence return 0, because there is no WebSocket hub without a slice.
    What to do
    Safe to leave in the code path; verify recipients on a slice.

So the whole authentication flow is a deployed-slice exercise. Structure a function that needs identity so its Deed calls sit behind one seam, and the rest stays testable on your laptop.

That is the single-function loop.

drift project run is the other one It boots the real slice runtime in Docker and serves every function plus your Canvas sites.