Using Auth
Sign-in flows, session handling, and organization context in the Quickback Stack
This page covers how to use Better Auth in your Quickback Stack application — sign-in flows, session management, and working with organization context.
Sign-In Flows
Better Auth supports multiple sign-in methods:
- Email/Password — Traditional credential-based authentication
- Email OTP — One-time passwords sent via email
- Magic Links — Passwordless email links
- Passkeys — WebAuthn biometric/hardware key authentication
Session Handling
Sessions live in the auth database — that is the source of truth. When your project uses the cloudflare-kv storage provider, the compiler wires that KV namespace up as Better Auth's secondaryStorage, which caches session lookups at the edge in front of the database. KV is a cache, not the store.
The generated auth middleware resolves the session once per request and hydrates a single Hono context variable, ctx — it does not set session or user variables:
// Access the auth context in a Hono route
app.get('/api/me', async (c) => {
const ctx = c.get('ctx');
if (!ctx.authenticated) return c.json({ error: 'Unauthorized' }, 401);
return c.json({
userId: ctx.userId, // Better Auth user id
user: ctx.user, // { id, email, name }
roles: ctx.roles, // org member roles: ['owner' | 'admin' | 'member']
});
});ctx also carries authMethod ('session' | 'jwt' | 'api-key' | 'oauth' | 'principal'), userRole (the global user-table role), activeTeamId, and scope. In action and CRUD handlers this same object arrives as the ctx argument — you rarely call c.get('ctx') directly.
Application Profiles
Treat the Better Auth user row as login identity, not automatically as the
application's complete client or customer model. Before defining feature
tables, decide whether signed-in people need domain data beyond the identity
already available on ctx.user—for example a customer number, onboarding
state, preferences, billing metadata, clinical details, or relationships that
other feature records must reference.
If the auth identity fields are enough, use ctx.userId / ctx.user directly
and do not create an empty duplicate profile. If the product needs domain data
or relationships, create a first-class feature resource such as customers,
clientProfiles, or patientProfiles. Give it its own feature-table primary
key and normal access, firewall, masking, and guard rules.
Use userId: q.text().required().index() for the basic link to the Better Auth
identity. Quickback emits the physical user_id column. Treat it as a plain
text logical foreign key: a self-create/bootstrap action derives it from
ctx.userId, and generic clients must not supply or update it through
guards.createable / guards.updatable. If the model permits one profile per
identity globally, add unique: [{ columns: ["userId"] }]; if it permits one
per organization, use unique: [{ columns: ["organizationId", "userId"] }]
instead. Declare explicitly when userId is also the row-ownership firewall
column rather than relying on its name.
Do not write .references(() => user.id) or .references(() => users.id) on
that link. With the default split-D1 provider, Better Auth lives in AUTH_DB
and feature resources live in DB, so SQLite cannot enforce a cross-database
foreign key. A provider may place both stores in one physical database, but the
auth schema remains compiler-owned rather than an authored feature-table
import; keeping this link logical makes the definition portable. Other feature
tables should use a real feature-database FK such as
profileId: q.text().required().references(() => profiles.id) so the CMS can
resolve the profile label. See Schema → References,
Firewall → ownerId vs userId, and
Start patterns → Referencing users.
Organization Context
When organizations are enabled, the active organization is on the auth context as activeOrgId:
const orgId = c.get('ctx').activeOrgId;`activeOrgId`, not `activeOrganizationId`
activeOrganizationId is Better Auth's session field name (session.session.activeOrganizationId). Quickback canonicalises it to ctx.activeOrgId; firewall rules written against ctx.activeOrganizationId are rewritten to ctx.activeOrgId at compile time.
Related
- Auth Overview — Setup and configuration
- Auth Plugins — Available plugins
- Auth Security — Security best practices