Version: closed alpha

Atomic

Serverless functions in the language you already know — no cold starts, no boilerplate.

Atomic runs your code as functions: you mark the ones you want to expose with a one-line directive, and Atomic builds them, gives each a URL, secures it, scales it, and keeps it responsive. There's no framework to learn and no server to configure — write a function, deploy it, call it.

Write a function

An @atomic directive — a comment placed directly above a function — tells the platform the HTTP method, the route, and the auth mode. Here's an endpoint that saves an item, in Go. The directive sits directly above the handler — the validate helper beside it has none, so it's free:

package main

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

// validate is a plain helper — it has no @atomic directive,
// so it's free and never counts as one of your functions.
func validate(body map[string]any) bool {
    return body["name"] != nil
}

// @atomic http=post:items auth=none
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.NoSQL.Collection("items").Insert(body)
    return 201, "Created", map[string]any{"id": id}, nil
}

A handler returns the status code, a short status message, the payload (serialised to JSON for you), and an optional headers map (nil when you don't need custom headers). The function's name is the method and route in your language's casing — http=post:itemsPostItems — and the directive does the routing, so there's no router to wire up.

Pay for what you expose, not what you write. The @atomic directive is the line between a billable endpoint and free scaffolding. The function directly below it is one of your function slots — it counts toward your plan and your bill. Every other function in your source — helpers, validation, shared logic — carries no directive, so it's free and doesn't count. Structure your code however you like; you only pay for the surface you choose to expose.

The @atomic directive

Every handler declares exactly one trigger, plus optional keywords on the same line:

TriggerMeaning
http=<method>:<route>An HTTP endpoint. Methods: get, post, put, delete, patch. Route params use :name (e.g. http=get:items/:id).
queue=<name>Runs when a message lands in the named Backbone queue.
cron="<expr>"Runs on a 5-field cron schedule (quoted), e.g. cron="0 * * * *".
KeywordValuesMeaning
auth=none · apikey · jwtProtection gate (see Authentication below). Defaults to none.
stream=sse · wsStream the response (Server-Sent Events or WebSocket).
secrets=KEY1,KEY2Inject the named Backbone secrets into this function as environment variables.

The comment marker can be //, #, or -- — whatever's natural for the language.

Six languages

Write functions in Go, Python, Node.js, Ruby, PHP, or Rust. The CLI detects the language from your source and handles the build for you — compiling Go and Rust, bundling the rest — and the SDK is always the latest, with nothing to pin. The same "save an item" handler in Python and Node:

import drift

# @atomic http=post:items auth=none
def post_items(body, req):
    item_id = drift.nosql.collection("items").insert(body)
    return 201, "Created", {"id": item_id}
const drift = require("@ondrift/sdk");

// @atomic http=post:items auth=none
async function postItems(body, req) {
    const id = await drift.nosql.collection("items").insert(body);
    return [201, "Created", { id }];
}

module.exports = { postItems };

In Python and Node a handler returns three values (status, message, payload); Go and Rust add the optional headers map. Everything your function needs — data, queues, secrets — is one drift.* call away. See the SDK reference →

Authentication

The auth keyword gates a function at the platform edge:

ModeBehaviour
auth=nonePublic — anyone can call it. The default.
auth=apikeyRequires a valid X-API-Key header. Missing → 401, wrong → 403.
auth=jwtRequires a valid Bearer token in the Authorization header, verified against the slice's own JWT signing key. Missing or invalid → 401.

Manage a function's API key from the CLI:

drift atomic auth set my-function <api-key>
drift atomic auth list my-function
drift atomic auth revoke my-function

For richer rules — sessions, roles, expiry — mint and verify tokens inside the function with the SDK's drift.JWT primitive (a per-slice signing key you never have to manage). See the Authentication guide → for the full login → protected-route flow.

Triggers

A function doesn't have to be called over HTTP. Swap the trigger in its directive:

// @atomic queue=orders auth=none      // runs for each message on the "orders" queue

# @atomic cron="0 2 * * *" auth=none     # runs at 02:00 every day (5-field cron)

Queue-triggered functions process messages as they arrive; cron functions run on a schedule. And since any HTTP function has a public URL, an http=post:… function is your webhook receiver — just point Stripe, GitHub, or your provider at it.

Routing

A function's address is the method and path from its @atomic directive: http=post:users answers only POST /api/users. 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 — two handlers, two @atomic directives, two billing units. The path /users is a human grouping, not the unit; the unit is the function. Each line in drift atomic list then says exactly what it is — one method, one path.

Streaming

Add stream=sse or stream=ws to push data to the client in real time:

package main

import (
    "time"

    drift "github.com/ondrift/sdk/go"
)

// @atomic http=get:events auth=none stream=sse
func GetEvents(req drift.Request, emit drift.Emitter) {
    emit.Send("tick", map[string]any{"at": time.Now()})
}

SSE works with the browser's native EventSource; stream=ws upgrades to a WebSocket where conn.Read() and conn.Write() exchange messages both ways. Both are available in every language.

Elements

An element is a single-language backend — Drift's unit of a "service" (what you'd put in one container elsewhere), minus the container. One language, one dependency manifest (go.mod / requirements.txt / package.json / …), and as many @atomic functions as you like across flat files in one folder.

The common case is the simplest there is: drop your handlers straight into atomic/ as flat files and the CLI auto-discovers every @atomic function — no per-function folders, no list to maintain. That flat set is an element (the implicit default one):

atomic/
├── auth.go        # post:signup · post:login · get:me
├── groups.go      # create · list · join · get
├── lib.go         # shared helpers — just a sibling file
└── go.mod         # one manifest for the whole backend

Need a second language? That's a second element — a second service. Give each its own folder under atomic/ (say atomic/api/ in Go and atomic/ml/ in Python), each with its own manifest. Mixing languages within one element isn't allowed; a second language is, by definition, a second element. Elements integrate through Backbone (queues, NoSQL, locks) — exactly the way microservices integrate through a broker, never by importing each other.

An element is an authoring + dependency boundary, not a URL one: routes always come from each function's @atomic directive, so the element name never appears in the path. Shared code (lib.go, crypto.go) is just an ordinary file in the same package — never a copy. Atomic supports Go, Python, Node, Ruby, and PHP as multi-function elements today. (Folder-per-function still deploys, if you prefer it — that's simply an element with one function.)

Deploy, run & inspect

# deploy a single function (optionally into an element)
drift atomic deploy ./atomic/post-items
drift atomic deploy ./atomic/list-items --element items

# run locally with hot reload before you ship
drift atomic run ./atomic/post-items

# inspect what's deployed
drift atomic list
drift atomic logs my-function
drift atomic metrics my-function

# roll back to a previous version
drift atomic history my-function
drift atomic rollback my-function <position>

Or declare every function in a Driftfile and ship them all at once with drift project deploy.