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.

Writing a handler

A Drift function is a named handler with an @atomic annotation. The CLI reads the annotation and generates the program's entry point, in every language. You write the handler and nothing else. No main(), no run() call, no route registration.

  • Annotation: @atomic http=post:items for an HTTP route, or @atomic queue=name for a queue consumer. Path params use :name.
  • Arguments: (req) for a request with no body (GET); (body, req) when there's a request body or queue payload.
  • Return: (status, message, payload). Go returns a fourth value, a map of response headers; the other five return exactly three.
  • Request: read path params from req.params, query from req.query, headers from req.headers, raw body from req.body.
  • Streaming: add stream=sse or stream=ws and the handler receives an extra emit (SSE) or conn (WebSocket) argument.
Go
// @atomic http=post:items auth=none
import drift "github.com/ondrift/cloud/sdk/go"

// Go only: the wrapper unmarshals the request body into this handler's
// own first-parameter type, read from your source. Each function in an
// element names its own body type, so two POST handlers never collide.
type PostItemsBody struct {
    Name string `json:"name"`
}

func PostItems(body PostItemsBody, req drift.Request) (int, string, any, map[string]string) {
    id, _ := drift.Backbone.NoSQL.Collection("items").Insert(body)
    return 201, "Created", map[string]any{"id": id}, nil
}

The annotation does the routing, so there is nothing to register and the handler's name is yours to choose, because the CLI reads it off the declaration the annotation sits above. The one-function-per-folder form (drift atomic deploy ./some-folder) is the exception: there the name is derived from the method and route, so http=post:items must be PostItems in Go and post_items in the snake_case languages. GET handlers take just (req).

In Go the body parameter can be any type you declare: the CLI reads the handler's signature out of your source and generates a wrapper that unmarshals into exactly that type. drift atomic new scaffolds <Handler>Body as a struct. Use map[string]any instead for a loose map. When the signature can't be read the wrapper falls back to a package-level RequestBody.

The request object

Every handler receives a req with the same fields in every language. The runner builds it from the inbound HTTP request and passes it to your function as JSON:

TypeWhat it holds
  • req.path
    Type
    string
    What it holds
    The request path, e.g. /api/items/42.
  • req.params
    Type
    map
    What it holds
    Path params from the route's :name segments, so http=get:items/:idreq.params["id"]. Extracted for you.
  • req.query
    Type
    string
    What it holds
    Raw, URL-encoded query string ("status=open&limit=50").not parsed. Split it with your language's URL library.
  • req.headers
    Type
    map
    What it holds
    Request headers by name, as in req.headers["Authorization"].
  • req.body
    Type
    any
    What it holds
    The parsed body: a JSON object for application/json, or a form dict for multipart/form-data and application/x-www-form-urlencoded (below). Also handed to you as the first body argument.
  • req.route_pattern
    Type
    string
    What it holds
    The registered pattern that matched, e.g. "items/:id". Set when a multi-handler element dispatches on it; empty otherwise.

Path params are extracted for you; the query string is not. A request to /api/orders?status=open arrives as req.query == "status=open", so parse it with url.ParseQuery (Go), URLSearchParams (Node), urllib.parse.parse_qs (Python), URI.decode_www_form (Ruby).

There is no req.method.

A function is addressed by method and path, and yours was routed here as the handler for one of them, so get:items and post:items are two separate functions. An off-method request 404s; it never reaches your code, so there is nothing to branch on.

Form posts and file uploads

The runner parses multipart/form-data and application/x-www-form-urlencoded for you, so body arrives as a dict either way. No parsing library needed. A field that repeats becomes a list.

In a multipart post, text fields are strings and each uploaded file is an object with four keys:

TypeWhat it holds
  • filename
    Type
    string
    What it holds
    The name the client sent, verbatim. Treat it as untrusted, and never join it into a path unsanitised.
  • content_type
    Type
    string
    What it holds
    The part's declared Content-Type. Also client-supplied.
  • data
    Type
    string
    What it holds
    Standard base64, not raw bytes. Decode it before you store or inspect it.
  • size
    Type
    integer
    What it holds
    Length of the decoded bytes. Check it before decoding to reject an oversized upload cheaply.

data is base64 and no SDK decodes it for you.

Passing it straight to blob.put stores the base64 text, not the file: roughly a third larger, and unreadable by anything that expects the original bytes. Use your language's own decoder: base64.b64decode (Python), Buffer.from(d, "base64") (Node), base64.StdEncoding.DecodeString (Go), Base64.strict_decode64 (Ruby), base64_decode (PHP).
Python
# @atomic http=post:upload auth=none
import base64

def post_upload(body, req):
    name = body["applicant_name"]              # a form field (string)
    f    = body["attachment"]                  # {filename, content_type, data, size}
    if f["size"] > 5 * 1024 * 1024:
        return 413, "Payload Too Large", {"error": "5MB maximum"}
    raw = base64.b64decode(f["data"])    # decode BEFORE storing
    drift.backbone.blob.put(f"uploads/{name}.pdf", raw, f["content_type"])
    return 201, "Stored", {"name": f["filename"], "bytes": f["size"]}

Return format

Every handler returns three values:

TypeDescription
  • 1
    Type
    integer
    Description
    HTTP status code (200, 201, 400, 500…)
  • 2
    Type
    string
    Description
    Short status message, such as "OK", "Created" or "Bad Request"
  • 3
    Type
    any
    Description
    Response body (objects and maps are serialised to JSON for you)
  • 4 (Go only)
    Type
    map[string]string
    Description
    Response headers. Pass nil for none.

It's a tuple in Go, Python, and Rust; an array in Node.js, PHP, and Ruby.

The fourth value is Go's alone.

Go's generated wrapper destructures four values; the wrapper for every other language destructures exactly three and has no header slot. Returning four from Python or Ruby raises an unpack error at the first request.

On the wire, the response body is an envelope.

For a JSON response the client receives {"status":…, "message":…, "payload":…}, not the bare payload. So a browser calling /api/<route> must read .payload (e.g. const data = (await res.json()).payload). The tuple's status also becomes the HTTP status code.

There is one escape from the envelope, and it is Go-only for the same reason: a handler that sets a non-JSON Content-Type header has its payload sent as the raw body, base64-decoded, with no envelope. That is how you return a file, an image, or a page of HTML. It keys off a response header, and only Go can set one, so from the other five languages, serve files from Canvas or hand back a blob URL.

Go
// @atomic http=get:report auth=none. Go: raw body, no envelope
func GetReport(req drift.Request) (int, string, any, map[string]string) {
    pdf, err := drift.Backbone.Blob.Get("reports/latest.pdf")
    if err != nil {
        return 404, "Not Found", map[string]any{"error": "no report yet"}, nil
    }
    // payload must be a base64 STRING when Content-Type isn't JSON
    b64 := base64.StdEncoding.EncodeToString(pdf)
    return 200, "OK", b64, map[string]string{"Content-Type": "application/pdf"}
}

The handler's own types

The CLI generates main() and calls the SDK's Run for you, in every language so you never call Run / run() yourself. A queue worker has the same shape as an HTTP handler; the queue message is delivered as the first body argument.

Go
// HTTP handler: body present (POST/PUT) or omit it (GET): func GetX(req drift.Request)
func PostItems(body PostItemsBody, req drift.Request) (int, string, any, map[string]string)

// Queue worker: same shape; the message is `body`. Trigger: // @atomic queue=validate
func Validate(body map[string]any, req drift.Request) (int, string, any, map[string]string)

type Request struct {
    // No Method field: you were routed here AS the post:items handler,
    // so there's nothing to branch on. get:x and post:x are two functions.
    Path         string
    Headers      map[string]string
    Query        string            // RAW query string, parse it yourself
    Body         json.RawMessage   // also handed to you as the `body` arg
    Params       map[string]string  // :name path params, extracted for you
    RoutePattern string            // the pattern that matched, for element dispatch
}

// Streaming args (stream=sse → emit, stream=ws → conn)
emit.Send(event string, data any)   ·   conn.ReadJSON(target any) bool   ·   conn.Write(data any)

What the handler can call once it is running is the API reference; how the same handler looks in each of the six languages is Languages.