Build
End-user auth
Every app has its own users. They sign up with email and password, or with a 6 digit code sent by email. A session is a short lived JWT access token plus a refresh token that rotates on every use.
Sessions
All auth routes live under /v1/apps/{app}/auth and need the app's publishable key (or its secret key). Signup, login, verify, and refresh all return the same session shape:
{
"user": {"id": "...", "email": "ada@example.com", "data": {"name": "Ada"}},
"access_token": "...",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "..."
}Email and password
Sign up. data is optional profile JSON you choose.
SESSION=$(curl -sS -X POST https://api.atberth.com/v1/apps/$APP/auth/signup \
-H "Authorization: Bearer $PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com","password":"correct horse battery","data":{"name":"Ada"}}')
ACCESS_TOKEN=$(echo "$SESSION" | jq -er .access_token)
REFRESH_TOKEN=$(echo "$SESSION" | jq -er .refresh_token)
USER_ID=$(echo "$SESSION" | jq -er .user.id)
echo "$USER_ID"Log in later with the same body minus data:
SESSION=$(curl -sS -X POST https://api.atberth.com/v1/apps/$APP/auth/login \
-H "Authorization: Bearer $PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com","password":"correct horse battery"}')
ACCESS_TOKEN=$(echo "$SESSION" | jq -er .access_token)
REFRESH_TOKEN=$(echo "$SESSION" | jq -er .refresh_token)The CLI can do both for testing: berth auth signup --app "$APP" --email ada@example.com --password '...' and berth auth login with the same flags.
Email code
No password at all: ask for a code, then trade it for a session. Asking creates the user if the email is new. Codes are valid for 10 minutes, one per minute per email, five attempts.
curl -sS -X POST https://api.atberth.com/v1/apps/$APP/auth/code \
-H "Authorization: Bearer $PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com"}'curl -sS -X POST https://api.atberth.com/v1/apps/$APP/auth/verify \
-H "Authorization: Bearer $PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com","code":"123456"}'The signed-in user
Send the publishable key in apikey and the user token in Authorization. The token alone also works.
curl -sS https://api.atberth.com/v1/apps/$APP/auth/me \
-H "apikey: $PUBLISHABLE_KEY" \
-H "Authorization: Bearer $ACCESS_TOKEN"Update the profile or the password:
curl -sS -X PATCH https://api.atberth.com/v1/apps/$APP/auth/me \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"data":{"name":"Ada Lovelace"}}'JWT claims
Access tokens are HS256 JWTs that expire after an hour. Your server can read the claims, but should check a token by calling /auth/me, since Berth holds the signing secret.
| Claim | Value |
|---|---|
sub | the user id |
email | the user's email |
aud | the app slug |
iss | berth |
sid | the session id, shared by the refresh chain |
exp | expiry, Unix seconds |
Look at the claims of the token you have:
echo "$ACCESS_TOKEN" | cut -d. -f2 | tr '_-' '/+' | jq -R '@base64d | fromjson'Refresh and rotation
When the access token is about to expire, trade the refresh token for a new session. Each refresh token works once: the response carries a new one.
OLD_REFRESH_TOKEN=$REFRESH_TOKEN
SESSION=$(curl -sS -X POST https://api.atberth.com/v1/apps/$APP/auth/refresh \
-H "Authorization: Bearer $PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d "{\"refresh_token\":\"$REFRESH_TOKEN\"}")
ACCESS_TOKEN=$(echo "$SESSION" | jq -er .access_token)
REFRESH_TOKEN=$(echo "$SESSION" | jq -er .refresh_token)If an old refresh token is ever used again, Berth treats it as stolen and revokes the whole chain, including the newest tokens. This answers 401:
curl -sS -X POST https://api.atberth.com/v1/apps/$APP/auth/refresh \
-H "Authorization: Bearer $PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d "{\"refresh_token\":\"$OLD_REFRESH_TOKEN\"}"After that the user signs in again. Store the refresh token in the Keychain or Keystore, and write the new one back every time you refresh.
SESSION=$(curl -sS -X POST https://api.atberth.com/v1/apps/$APP/auth/login \
-H "Authorization: Bearer $PUBLISHABLE_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com","password":"correct horse battery"}')
ACCESS_TOKEN=$(echo "$SESSION" | jq -er .access_token)
REFRESH_TOKEN=$(echo "$SESSION" | jq -er .refresh_token)Sign out
Ends this session. Send {"all": true} to end every session of the user.
curl -sS -X POST https://api.atberth.com/v1/apps/$APP/auth/logout \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"all":false}'Managing users from your server
With the secret key you can list, create, ban, and delete users. Deleting a user also deletes the rows they own.
berth auth users list --app "$APP"USER_ID=$(curl -sS -X POST https://api.atberth.com/v1/apps/$APP/auth/users \
-H "Authorization: Bearer $SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"grace@example.com","password":"another long password"}' | jq -er .user.id)
berth auth users show --app "$APP" "$USER_ID"curl -sS -X PATCH https://api.atberth.com/v1/apps/$APP/auth/users/$USER_ID \
-H "Authorization: Bearer $SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"banned":true}'
berth auth users ban --app "$APP" "$USER_ID" --unbanberth auth users delete --app "$APP" "$USER_ID" --yesCalling from a phone or browser
Ship the base URL and the publishable key. Nothing else. Then sign in and send the user token on each request.
JavaScript (fetch)
const BASE = "https://api.atberth.com/v1/apps/notes_app";
const KEY = "bpk_..."; // publishable key only
async function login(email, password) {
const res = await fetch(`${BASE}/auth/login`, {
method: "POST",
headers: { "apikey": KEY, "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!res.ok) throw new Error((await res.json()).message);
return res.json(); // { user, access_token, refresh_token, ... }
}
async function myNotes(session) {
const res = await fetch(`${BASE}/tables/notes/rows?order=created_at.desc`, {
headers: { "apikey": KEY, "Authorization": `Bearer ${session.access_token}` },
});
return (await res.json()).rows;
}Swift
struct Session: Decodable {
let access_token: String
let refresh_token: String
let expires_in: Int
}
let base = URL(string: "https://api.atberth.com/v1/apps/notes_app")!
let publishableKey = "bpk_..."
func login(email: String, password: String) async throws -> Session {
var req = URLRequest(url: base.appending(path: "auth/login"))
req.httpMethod = "POST"
req.setValue(publishableKey, forHTTPHeaderField: "apikey")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONEncoder().encode(["email": email, "password": password])
let (data, response) = try await URLSession.shared.data(for: req)
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw URLError(.userAuthenticationRequired) }
return try JSONDecoder().decode(Session.self, from: data)
}
func notes(_ session: Session) async throws -> Data {
var req = URLRequest(url: base.appending(path: "tables/notes/rows"))
req.setValue(publishableKey, forHTTPHeaderField: "apikey")
req.setValue("Bearer \(session.access_token)", forHTTPHeaderField: "Authorization")
return try await URLSession.shared.data(for: req).0
}Kotlin (OkHttp)
val base = "https://api.atberth.com/v1/apps/notes_app"
val publishableKey = "bpk_..."
val client = OkHttpClient()
val json = "application/json".toMediaType()
fun login(email: String, password: String): JSONObject {
val body = JSONObject().put("email", email).put("password", password)
val req = Request.Builder()
.url("$base/auth/login")
.header("apikey", publishableKey)
.post(body.toString().toRequestBody(json))
.build()
client.newCall(req).execute().use { res ->
check(res.isSuccessful) { "login failed: ${res.code}" }
return JSONObject(res.body!!.string())
}
}
fun notes(accessToken: String): String {
val req = Request.Builder()
.url("$base/tables/notes/rows")
.header("apikey", publishableKey)
.header("Authorization", "Bearer $accessToken")
.build()
client.newCall(req).execute().use { return it.body!!.string() }
}Keep the refresh token in the iOS Keychain or Android Keystore backed storage, never in plain preferences.