Auth
Sign users in for the components — Better Auth cookies in the browser, bearer tokens in React Native and other non-browser clients.
The components never touch credentials. The emitted client you pass to
<QuickbackProvider client> decides how each request is authenticated.
| Mode | Use it for | Credential | Writes need |
|---|---|---|---|
auth: "cookie" (default) | Browser apps | Better Auth session cookie, credentials: "include" | The page's origin in the trusted origins |
auth: "bearer" | React Native, CLIs, servers | Authorization: Bearer <token> | Nothing extra. Bearer requests skip the CSRF origin check. |
Browser: cookie
Sign in with Better Auth's own client. The session cookie it sets is what the Quickback client sends.
import { createAuthClient } from "better-auth/react"
import { organizationClient } from "better-auth/client/plugins"
import { createClient } from "@/lib/quickback.client"
const baseURL = import.meta.env.VITE_API_URL // e.g. http://localhost:8787
export const auth = createAuthClient({
baseURL,
basePath: "/auth/v1",
plugins: [organizationClient()],
})
export const client = createClient({ baseUrl: baseURL })
await auth.signIn.email({ email, password })
// Tenant-scoped tables are firewalled to the session's active organization.
await auth.organization.setActive({ organizationId })The cookie only works if the API trusts your page's origin:
- Same Worker (recommended). Mount the built app with
apps.<name>. It is then served on the same origin as/apiand/auth, and its hostname is trusted automatically. - Local dev. Until a project has a production domain,
http://localhost:5173(Vite),:3000, and:8787are trusted by default. - Anywhere else. Add the origin to
trustedOrigins. A write from an untrusted origin fails with403 CSRF_ORIGIN_REJECTED, and the browser blocks reads through CORS.
React Native and other non-browser clients: bearer
React Native has no cookie jar and sends no Origin header. Cookie-backed
writes therefore fail with 403 CSRF_ORIGIN_REJECTED. Use the bearer lane
instead:
import * as SecureStore from "expo-secure-store"
import { createClient } from "@/lib/quickback.client"
const KEY = "quickback_session"
export const client = createClient({
baseUrl: "https://api.example.com",
auth: "bearer",
sessionStore: {
get: () => SecureStore.getItemAsync(KEY),
set: (token) => (token ? SecureStore.setItemAsync(KEY, token) : SecureStore.deleteItemAsync(KEY)),
},
})
// set-auth-token on /auth/v1/* is saved to sessionStore.
await client.request("POST", "/auth/v1/sign-in/email", { body: { email, password } })
await client.request("POST", "/auth/v1/organization/set-active", { body: { organizationId } })Sign in through the client, as above. It saves the set-auth-token
header: the signed <token>.<signature>. If you sign in with your own
fetch, store that header too, never the body's token. The body carries
the raw session id, and the generated API runs Better Auth's
bearer({ requireSignature: true }). An unsigned token is silently ignored:
get-session returns null and every API call answers 401.
With auth: "bearer", the client:
- sends
Authorization: Bearer <token>and defaultscredentialsto"omit"; - caches the short-lived JWT that API responses return in
set-auth-token(see JWT optimization); - on a
401with a cached JWT, drops the JWT and retries once with the session token fromsessionStore.
Full lane semantics: Bearer lane.