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
Route gates, API keys and your account.
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.

Deed KeyAuth

Passwordless authentication built on device key pairs (Ed25519), with no passwords to store or leak. The user's public key is their identity. The flow is a two-step handshake: the slice mints a one-time challenge nonce, the client signs it with its private key, and verify checks the signature and returns your slice's own session JWT.

Go
// Driftfile: name post:auth/challenge, handler PostAuthChallenge
nonce, _ := drift.Deed.KeyAuth.Challenge(pubkey)   // single use, 120s
slice := drift.Deed.JWT.SliceID()               // the client signs this too; return both

// Driftfile: name post:auth/verify, handler PostAuthVerify
token, _ := drift.Deed.KeyAuth.Verify(pubkey, sig, "my-app")   // → this slice's JWT

The domain argument namespaces the signature per app, so a signature made for one app can't be replayed against another on the same slice. The signed message also carries this slice's own identity, filled in by the slice itself rather than trusted from the request: every slice on the platform runs the identical binary, so without that field two slices would sign and verify identical bytes for the same domain, nonce and device key. The private key never leaves the device; the slice only ever sees public keys and signatures, and stores nothing about the user. The token's sub is the identity and it expires 30 days out. A device that Link has revoked is refused here even though its signature is genuinely valid.

Signing the challenge in the browser

The slice verifies a signature over one exact byte string: compact JSON, keys in sorted order, no whitespace, over four fields: domain, nonce, pubkey and slice. Build it literally rather than trusting a serializer's key ordering.

Node.js
const hex = b => [...new Uint8Array(b)].map(x => x.toString(16).padStart(2, "0")).join("");
const domain = "my-app";   // the same string your Verify call passes

// 1. one keypair per device. The private key is non-extractable and never leaves.
const pair = await crypto.subtle.generateKey({ name: "Ed25519" }, false, ["sign", "verify"]);
const pubkey = hex(await crypto.subtle.exportKey("raw", pair.publicKey));   // 64 hex chars

// 2. ask your own function for a nonce AND this slice's identity
const { nonce, slice } = await (await fetch("/api/auth/challenge", {
  method: "POST", body: JSON.stringify({ pubkey }),
})).json();

// 3. sign the canonical message: this exact string, nothing else
const msg = `{"domain":${JSON.stringify(domain)},"nonce":${JSON.stringify(nonce)},"pubkey":${JSON.stringify(pubkey)},"slice":${JSON.stringify(slice)}}`;
const bytes = new TextEncoder().encode(msg);
const sig = hex(await crypto.subtle.sign({ name: "Ed25519" }, pair.privateKey, bytes));

// 4. POST pubkey + sig to your verify function; it hands back the slice's JWT

The signed message must match byte for byte.

One that differs by a single byte (a space after a colon, a different key order, a domain or slice value the function did not pass through) comes back as 401 bad signature with no further detail. The client and Verify must use the same domain string, and there is no platform-wide fallback for an empty one: a client that signs an empty domain must call Verify with an empty one too, and vice versa.

Authentication walks the same flow end to end, from login to a protected route.