Appearance
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/RedisKey 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:
- Converts AdonisJS request → Web API Request
- Passes to Better Auth handler
- 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.localhostAPI 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_verifiedinstead ofemailVerifiedcreated_atinstead ofcreatedAtuser_idinstead ofuserId
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:
SignInFormcalls the webappuseAuth().signIn()wrapper@jubiloop/auth-client(Better Auth client) →POST /auth/sign-in/email(no/apiprefix — subdomain routes directly)BetterAuthController.handle()- Converts AdonisJS request → Web API
Request - Delegates to Better Auth handler
- Converts AdonisJS request → Web API
- 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
sessionstable - Sets Better Auth's
jubiloop-prefixed HTTP-only session cookie - Uses the configured
DOMAINvalue for cross-subdomain cookies
- Response returns to webapp
- Browser stores the session cookie automatically
- 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 @$!%*?&.