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:items → PostItems — 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:
| Trigger | Meaning |
|---|---|
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 * * * *". |
| Keyword | Values | Meaning |
|---|---|---|
auth= | none · apikey · jwt | Protection gate (see Authentication below). Defaults to none. |
stream= | sse · ws | Stream the response (Server-Sent Events or WebSocket). |
secrets= | KEY1,KEY2 | Inject 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:
| Mode | Behaviour |
|---|---|
auth=none | Public — anyone can call it. The default. |
auth=apikey | Requires a valid X-API-Key header. Missing → 401, wrong → 403. |
auth=jwt | Requires 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.