Skip to content

Authentication Backend Implementation

Architecture

Authentication is handled by Better Auth integrated with AdonisJS. All auth routes are proxied through a single controller that converts between AdonisJS and Better Auth's Web API format.

Client Request → AdonisJS Router → BetterAuthController → Better Auth → Database/Redis

Key Components

Better Auth Configuration (app/lib/auth.ts)

  • Database: PostgreSQL connection pool
  • Runtime Session Storage: Redis secondary storage, plus a signed 5-minute cookie cache
  • Authentication: Email/password with Argon2 hashing
  • Organizations: Multi-tenant workspace support
  • Sessions: 7-day expiry with a 1-day refresh interval

Controller (app/controllers/better_auth_controller.ts)

Single handler that:

  1. Converts AdonisJS request → Web API Request
  2. Passes to Better Auth handler
  3. Converts Web API Response → AdonisJS response

Route Configuration (start/routes.ts)

typescript
// All auth routes handled by Better Auth
router.any('/auth/*', [BetterAuthController, 'handle']).use(apiThrottle)

Data Models

Authentication uses users and credential accounts in PostgreSQL. Compatibility tables also exist for sessions and verification values, while the current runtime values live in Redis. Organization, membership, invitation, and team persistence is documented separately.

Security Implementation

Password Security

  • Hashing: Argon2 (industry standard)
  • Verification: Argon2 verifies the stored account hash
  • No plaintext storage: Passwords never stored unhashed

Session Security

  • HTTP-only cookies: JavaScript cannot access tokens
  • Secure cookies: HTTPS-only in production
  • Cross-subdomain: Works across app/api subdomains
  • Rate limiting: Built-in protection against brute force

Database Security

  • UUID primary keys: No sequential ID leaking
  • Connection pooling: Efficient database connections
  • Prepared statements: SQL injection protection

Redis Configuration

Used as Better Auth's runtime secondary storage:

typescript
secondaryStorage: {
  get: async (key) => (await redis.get(key)) || null,
  set: async (key, value, ttl) => {
    if (ttl) await redis.set(key, value, 'EX', ttl)
    else await redis.set(key, value)
  },
  delete: async (key) => redis.del(key)
}

Benefits:

  • Runtime session lookup: Better Auth can read sessions from Redis secondary storage
  • Automatic expiry: TTL-based session cleanup
  • Scalability: Shared state across multiple server instances

Organization Features

Authentication supplies the active organization and team IDs on the session. Organization limits, roles, ownership rules, invitations, and team behavior are documented in the organization backend guide.

Environment Configuration

Required environment variables:

bash
# Database
DB_HOST=localhost
DB_PORT=5433
DB_USER=jubiloop
DB_PASSWORD=password
DB_DATABASE=jubiloop

# Server
SERVER_URL=https://api.jubiloop.localhost
DOMAIN=jubiloop.localhost

# Security
ALLOWED_ORIGINS=https://app.jubiloop.localhost,https://jubiloop.localhost

API Endpoints

All authentication and organization endpoints are prefixed with /auth/ and handled by Better Auth. See the Authentication API reference for request bodies, response shapes, status codes, and enabled organization operations.

Database Naming Conventions

Better Auth is configured to use AdonisJS snake_case conventions:

  • email_verified instead of emailVerified
  • created_at instead of createdAt
  • user_id instead of userId

This ensures consistency with the rest of the AdonisJS application.

End-to-End Sign-In Flow

Tracing a sign-in from the UI to the database and back:

  1. SignInForm calls the webapp useAuth().signIn() wrapper
  2. @jubiloop/auth-client (Better Auth client) → POST /auth/sign-in/email (no /api prefix — subdomain routes directly)
  3. BetterAuthController.handle()
    • Converts AdonisJS request → Web API Request
    • Delegates to Better Auth handler
  4. Better Auth
    • Looks up user by email in PostgreSQL
    • Verifies password with Argon2
    • Creates the runtime session in Redis; the current configuration does not persist it to the PostgreSQL sessions table
    • Sets Better Auth's jubiloop-prefixed HTTP-only session cookie
    • Uses the configured DOMAIN value for cross-subdomain cookies
  5. Response returns to webapp
    • Browser stores the session cookie automatically
  6. Webapp
    • The shared hook invalidates the auth query scope
    • api.auth.getSessionData(queryClient) refetches (cache-first)
    • api.auth.isAuthenticated(data)true
    • React UI reflects authenticated state

Password Reset Flow

The webapp calls requestPasswordReset with a redirect to /reset-password. Better Auth creates a one-hour verification value in Redis and attempts to send the reset link through Adonis Mail and Resend. Mail delivery failures are logged without changing the public response. The reset page reads the token query parameter and submits it to Better Auth. The webapp requires at least eight characters, including a lowercase letter, uppercase letter, digit, and one of @$!%*?&.

Built with ❤️ by the Jubiloop team