Auth API

REST endpoints for sign up, sign in, sessions, password reset, OAuth, and API key management.

#Auth API

Authentication endpoints live under /api/auth/ and are not version-prefixed. They handle user registration, sign-in flows, session management, and API key operations.

Note

Auth routes are powered by Better Auth running natively on Cloudflare Workers.

#Sign up

POST /api/auth/sign-up/email

Create a new user account with email and password. Registration availability is controlled by RBAC — see Registration below.

#Parameters

Parameter Type Required Description
email string Yes User's email address
password string Yes Password (hashed server-side with PBKDF2 via Web Crypto)
name string Yes Display name

#Request

#HTTP

POST /api/auth/sign-up/email
Content-Type: application/json

{
  "email": "alex@example.com",
  "password": "securePassword123",
  "name": "Alex Chen"
}

#SDK

import { EmuView } from '@emuview/sdk';

const sdk = new EmuView({
	url: 'https://your-api.example.com'
});

const session = await sdk.auth.signUp({
	email: 'alex@example.com',
	password: 'securePassword123',
	name: 'Alex Chen'
});
// session.user → { id, email, name, role }
// session.session → { id, expiresAt }

#Response

200 OK

{
	"token": "eyJhbGciOiJIUzI1NiIs...",
	"user": {
		"id": "usr_01HXK5M9ABCDEF",
		"email": "alex@example.com",
		"name": "Alex Chen",
		"role": "viewer"
	}
}

#Errors

Status Code Description
400 invalid_request Missing or malformed email/password
403 signup_forbidden Registration is closed — the public role lacks system/users:create
409 already_exists An account with this email already exists
429 rate_limited Too many sign-up attempts (5 per minute per IP)

#Registration

Who can register is controlled by RBAC, not an environment variable. A sign-up request is checked as the public role, so it succeeds only when public holds the system/users:create permission:

Policy How to set it Behaviour
Open Grant publicsystem/users:create (choose "Open" at setup, or toggle it in Access → Roles → public) Anyone can register and joins the shared workspace as viewer
Closed (default) Leave public without the grant Sign-up returns 403 signup_forbidden; an admin creates accounts via the users UI or POST /api/v1/users

The very first sign-up on a fresh install is always allowed — it bootstraps the instance and its owner regardless of this grant.


#Sign in

POST /api/auth/sign-in/email

Authenticate with email and password to receive a session token.

#Parameters

Parameter Type Required Description
email string Yes Account email address
password string Yes Account password

#Request

#HTTP

POST /api/auth/sign-in/email
Content-Type: application/json

{
  "email": "alex@example.com",
  "password": "securePassword123"
}

#SDK

const session = await sdk.auth.signIn({
	email: 'alex@example.com',
	password: 'securePassword123'
});
// The SDK stores the session token automatically for future calls

#Response

200 OK

{
	"token": "eyJhbGciOiJIUzI1NiIs...",
	"user": {
		"id": "usr_01HXK5M9ABCDEF",
		"email": "alex@example.com",
		"name": "Alex Chen",
		"role": "admin"
	}
}

#Errors

Status Code Description
401 unauthorized Invalid email or password
403 mfa_required Account has TOTP enabled; provide the TOTP code
403 account_banned Account has been banned by an admin
429 rate_limited Too many sign-in attempts (5 per minute per IP)

#Username sign-in (opt-in)

These endpoints exist only while username sign-in is enabled in Settings → Authentication; otherwise they return 404.

POST /api/auth/sign-in/username — sign in with { username, password }. Response and errors match email sign-in (same generic 401 for unknown-user and wrong-password).

POST /api/auth/is-username-available — check { username }, returns { "available": boolean }. Shares the auth rate limit; call on blur/submit, not per keystroke.

GET /api/auth/capabilities — public, always available. Returns which auth features the instance has enabled, for pre-auth login UIs:

{
	"usernameEnabled": true,
	"requireEmail": false,
	"socialProviders": { "google": true, "github": false }
}

POST /api/auth/request-password-reset — initiate a password reset with { identifier, redirectTo? }, where identifier is an email or username ({ email } is also accepted). Always returns 200 { "status": true } regardless of whether the account exists (anti-enumeration). Accounts whose only address is a shared contact email get a reset mail naming the account it applies to.

POST /api/v1/users/:id/reset-link — admin-only (system/users:update). Returns { url, expiresAt }, a one-time password-reset link valid for 1 hour, for accounts that can't receive email. Audit-logged as user.recovery_link_issued.


#Get session

GET /api/auth/get-session

Check the current session and retrieve the authenticated user's details.

#Request

#HTTP

GET /api/auth/get-session
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

#SDK

const current = await sdk.auth.getSession();
if (!current) {
	// Not logged in — redirect to login page
}

#Response

200 OK

{
	"user": {
		"id": "usr_01HXK5M9ABCDEF",
		"email": "alex@example.com",
		"name": "Alex Chen",
		"role": "admin",
		"projectId": "proj_abc"
	},
	"session": {
		"id": "ses_01HXK5PQRSTUV",
		"expiresAt": 1719504000
	}
}

#Errors

Status Code Description
401 unauthorized Missing, expired, or invalid session token

#Sign out

POST /api/auth/sign-out

Invalidate the current session token.

#Request

#HTTP

POST /api/auth/sign-out
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

#SDK

await sdk.auth.signOut();

#Response

200 OK

{ "success": true }

#OAuth callback

GET /api/auth/callback/:provider

OAuth redirect endpoint for social sign-in. This is not called directly — EmuView redirects the user's browser here after they authenticate with the provider.

#Supported providers

Provider Redirect URL Required env vars
Google /api/auth/callback/google GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
GitHub /api/auth/callback/github GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET

#Starting an OAuth flow

Redirect the user's browser to the provider's authorize URL. EmuView handles the callback and creates or links the account automatically.

#HTTP

GET /api/auth/sign-in/social?provider=google&callbackURL=https://your-app.example.com/auth/callback

#SDK

// In a browser context, redirect the user to start OAuth
const url = sdk.auth.getOAuthUrl('google', {
	callbackURL: 'https://your-app.example.com/auth/callback'
});
window.location.href = url;

#Response

On successful authentication, the callback redirects to your callbackURL with a session token. If the user's email doesn't have an existing account, one is created automatically (when the public role holds system/users:create — see Registration).


#API keys

API keys provide server-side access without a user session. Keys are prefixed with sk- and the raw key is only shown once at creation — EmuView stores a SHA-256 hash.

#Create API key

POST /api/auth/api-keys

#Parameters

Parameter Type Required Description
label string Yes Human-readable label for the key
capabilities string[] Yes Permission scopes (e.g., ["read:products", "write:orders"])
expires_at integer No Unix timestamp when the key expires (omit for no expiry)

#Request

#HTTP
POST /api/auth/api-keys
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

{
  "label": "Production frontend",
  "capabilities": ["read:products", "read:categories"],
  "expires_at": null
}
#SDK
const key = await sdk.auth.createApiKey({
	label: 'Production frontend',
	capabilities: ['read:products', 'read:categories']
});
// key.raw → "sk-a1b2c3d4..." — save this, it won't be shown again

#Response

201 Created

{
	"id": "key_01HXK5M9ABCDEF",
	"label": "Production frontend",
	"capabilities": ["read:products", "read:categories"],
	"raw": "sk-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
	"created_at": 1718900000,
	"expires_at": null
}
Warning

The raw field contains the full API key and is only returned at creation time. Store it securely — you cannot retrieve it again.

#List API keys

GET /api/auth/api-keys

Returns all API keys for the current project. The raw key is never included.

#Response

200 OK

[
	{
		"id": "key_01HXK5M9ABCDEF",
		"label": "Production frontend",
		"capabilities": ["read:products", "read:categories"],
		"last_used_at": 1718905000,
		"created_at": 1718900000,
		"expires_at": null
	}
]

#Revoke API key

DELETE /api/auth/api-keys/:id

Permanently revoke an API key. Any requests using this key return 401 immediately.

#Request

DELETE /api/auth/api-keys/key_01HXK5M9ABCDEF
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

#Response

200 OK

{ "success": true }

#Capability tokens

POST /api/v1/capability-tokens

A short-lived bearer token naming one document and one capability. It exists so a separate worker — a realtime document room, say — can decide what a WebSocket connection may do before it accepts it, without holding the deployment's signing secret and without asking the gateway again per message.

Nothing about the token is a second access-control system. The gateway asks its ordinary permission evaluator, on the caller's behalf, for every action the requested capability implies, and signs the answer. A token can never carry more than the caller could already do by hand.

#Request

POST /api/v1/capability-tokens
Authorization: Bearer <session token or sk- key>
Content-Type: application/json

{
  "collection": "dms_documents",
  "documentId": "01JABCDEFGHIJKLMNOPQRSTUVW",
  "capability": "write",
  "expiresIn": 300
}
Field Required Notes
collection yes Collection holding the document.
documentId yes The record's id.
capability no read (default), comment or write. An omitted field always defaults to the weakest.
expiresIn no Requested lifetime in seconds. Clamped to 30–600; the granted value comes back in the response.

#What each capability requires

Capability Grants needed on collections/<name>
read read
comment read + update
write read + update

comment needs the same grants as write deliberately: RBAC has no verb meaning "may annotate but not edit", so a commenter is a writer who asked for less. write needs read because a document session streams the whole document before it can edit anything.

The row matters as much as the verb. The document must be reachable under the item_filter of each grant separately, so a wider read scope cannot vouch for a narrower update one.

#Response

201 Created

{
	"data": {
		"token": "sct1.eyJ2Ijoic2N0MSIs...",
		"capability": "write",
		"document": {
			"collection": "dms_documents",
			"id": "01JABCDEFGHIJKLMNOPQRSTUVW",
			"type": "record"
		},
		"subject": "usr_01HXK5M9ABCDEF",
		"issuedAt": 1756000000,
		"expiresAt": 1756000300,
		"expiresIn": 300,
		"maxExpiresIn": 600
	}
}

The response is Cache-Control: no-store. The token is returned exactly once, here. There is no GET on this endpoint and nothing stores the token — not the audit log, not a log line — so it cannot be read back later. Treat it the way you would treat a password.

#Refusals

Status Meaning
401 No credential. An anonymous caller is never minted a token.
403 The caller does not hold a grant the capability needs. The body names the missing resource and action. Nothing is downgraded — asking for write with only read is refused, not quietly answered with a read token.
404 No such document, or one outside the caller's row scope. The two are deliberately indistinguishable: this endpoint takes an arbitrary id, and a distinguishable refusal would let anyone enumerate documents.
400 Malformed body, unknown capability, or an unusable collection name or document id.

#Expiry is the revocation story

There is no revocation list, by design. The lifetime is at most 10 minutes and 5 by default. Revoking a share stops new tokens immediately, because every mint asks the live evaluator; a connection already open converges when it next reconnects. If you need a tighter window, ask for a shorter expiresIn — the server will never grant a longer one than you asked for, only a shorter one than you asked for if you exceeded the ceiling.


#Session payload

When the API resolves a session token or API key, it produces the following identity object used for all permission checks:

interface AuthUser {
	id: string; // Better Auth user ID
	email: string;
	role: UserRole; // 'super_admin' | 'admin' | 'editor' | 'viewer' | 'api'
	projectId: string; // The project this user belongs to
}

#Roles

Role Access level
super_admin Instance-level; sees all projects, manages all users
admin Project-level; manages collections, users, settings
editor Project-level; read/write on permitted collections
viewer Project-level; read-only on permitted collections
api API key identity; capabilities scoped per key