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
Accounts, tokens and scopes.
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.

Write a function

A function is an ordinary function in your source, plus one entry in your Driftfile naming it. Nothing in the code marks it — no decorator, no magic comment, no base class. Here's an endpoint that saves an item, in Go, with a validate helper beside it:

atomic/items.go
package main

import drift "github.com/ondrift/cloud/sdk/go"

// validate is a plain helper. The Driftfile never names it,
// so it never counts as one of your functions.
func validate(body map[string]any) bool {
    return body["name"] != nil
}

func PostItems(body map[string]any, req drift.Request) (int, string, any, map[string]string) {
    if !validate(body) {
        return 400, "Bad Request", map[string]any{"error": "name required"}, nil
    }
    id, _ := drift.Backbone.NoSQL.Collection("items").Insert(body)
    return 201, "Created", map[string]any{"id": id}, nil
}

And the entry that turns PostItems into an endpoint:

Driftfile
atomic:
  functions:
    - route: items
      method: post
      handler: PostItems

A handler returns the status code, a short status message and the payload (serialised to JSON for you), plus an optional fourth value: a map of response headers. Leave it out when you don't need one — Go takes nil, and in Python, Node, Ruby and PHP a three-value return is complete on its own.

Rust is the one that sets headers differently. Its entry point destructures a fixed-size tuple, so a fourth value there would stop every existing handler compiling; call drift_sdk::set_response_header(name, value) instead and the return stays three values.

To return a body with no envelope, declare the shape.

By default a JSON payload reaches the caller wrapped as {"status": …, "message": …, "payload": …}. Add response: to the function's Driftfile entry to change that — see What an entry declares below. Setting a Content-Type that isn't JSON also writes the payload straight to the wire, which is how this worked before response existed; it still does, but declare response: raw instead.

Handlers that take a body (post, put, delete, patch and queue handlers) are called as (body, req). A get handler is called as (req).

Pay for what you expose, not what you write.

The Driftfile entry is the line between a billable endpoint and free scaffolding. A function the manifest names is one of your function slots, so it counts toward your plan and your bill. Every other function in your source — helpers, validation, shared logic — is free and doesn't count. Structure your code however you like; you only pay for the surface you choose to expose.

What an entry declares

Each entry in atomic.functions is the whole declaration for one function. Two fields are always required and the rest have defaults:

Field Meaning
nameWhat the function answers on: post:items, get:groups/:id, or queue:orders for one the slice invokes from a queue. This is the identity Drift books, prices and meters.
handlerThe callable that serves it.
memoryIts own pool, 8MB–256MB. No default — see Memory and concurrency.
authnone (default) or apikey. See Authentication.
streamsse or ws to hold the connection open. See Streaming.
responseThe shape of this function's replies: envelope (default), json or raw. See below.
secretsThe Backbone secrets this one function may read.
elementWhich element it belongs to. Omitted means the flat files in atomic/.
cronA schedule it fires on in addition to its own trigger. See Triggers.

The shape of a reply

Most endpoints answer other code you wrote, and the envelope is what the SDKs expect. An endpoint a third party parses usually cannot take it: a wallet, an OAuth client or a webhook sender wants your keys at the top level, in the shape its own spec names. response says which you are building.

What reaches the callerContent-Type
  • envelope (default)
    What reaches the caller
    {"status": …, "message": …, "payload": …}
    Content-Type
    application/json
  • json
    What reaches the caller
    your payload, serialised — the whole body
    Content-Type
    application/json, unless you set one
  • raw
    What reaches the caller
    your payload as bytes, base64-decoded first
    Content-Type
    yours, and required

An OAuth token endpoint is the case this exists for — the spec pins it to application/json, which is also the envelope's own type, so nothing about the response itself can distinguish them:

Driftfile
    - route: token
      method: post
      handler: PostToken
      response: json
Node.js
async function PostToken(body, req) {
  return [200, "OK", { access_token: "…", token_type: "Bearer", expires_in: 3600 }];
}

reaches the caller as {"access_token":"…","token_type":"Bearer","expires_in":3600} and nothing else.

Use raw for a body that isn't text at all — an image, a PDF, a signed token — and set your own Content-Type alongside it. Your payload is base64 there, because bytes have to survive a JSON hop to get to Drift.

How a handler is found

Drift looks for handler in the element's directory — atomic/ for the default element, atomic/<element> for a named one — and nowhere else. Two elements can each have a handle; within one element the name has to be unique.

The element's directory is also the whole of what gets deployed.

Drift archives that one folder and ships it. A file your handler imports from outside it — ../dist/bundle.js, a sibling lib/, a config at the repo root — exists on your machine and is simply not there at runtime, which surfaces as a module-not-found or ENOENT in the deployed function rather than as a deploy error. Bundle what you need into the element, or give the shared code its own element.

The callable has to be reachable from outside its own file, because the entry point Drift generates imports it by name:

Language Shape
Gofunc PostItems(…), exported, capital first letter
Pythondef post_items(…):
Nodefunction postItems(…), a named declaration; an arrow assigned to a const is not matched
Rubydef post_items(…)
PHPfunction postItems(…)
Rustpub fn post_items(…), where pub is required

The name is yours to choose. A handler the Driftfile names and the source doesn't have fails the deploy before anything is built, and the error lists the callables that directory does declare.

Node means JavaScript. Drift discovers .js, .mjs and .cjs; there is no TypeScript compiler in the deploy path, so a .ts file in an element is never looked at and its handlers are never found. Compile or bundle to JavaScript first — a pre_deploy hook is the usual place — and ship the output.

Routing

A function's address is the method and path from its name: post:items answers only POST /api/items. The method is part of the identity, not a detail. A request with a different method to the same path doesn't fall through to this function, it 404s. There's nothing to branch on inside the handler — you were routed here because you're the POST handler — so drift.Request deliberately doesn't expose the method.

So get:users (list) and post:users (create) are two functions with two handlers, two entries and two billing units. The path /users is a human grouping, not the unit; the unit is the function.

The /api space is shared across every element, so two entries declaring the same name collide wherever they live. drift file apply refuses the whole project and names both.

Method-and-path identity is why most CLI commands that address a single function take --method: without it, get:users and post:users are indistinguishable by name.