Quickback Docs

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.

On this page