Documentation Index Fetch the complete documentation index at: https://mintlify.com/Ishaq74/concordia/llms.txt
Use this file to discover all available pages before exploring further.
Concordia uses Better Auth for authentication and session management, configured with email/password authentication, organizations, and advanced security features.
Overview
The authentication system provides:
Email/password authentication with validation
Email verification for new accounts
Password reset flow
Session management with automatic refresh
Rate limiting for security
Organization support with invitations
Admin impersonation capabilities
Comprehensive audit logging
Authentication Setup
The auth configuration is centralized in src/lib/auth/auth.ts. Two instances are created:
API Instance (async): Used by the Astro application
CLI Instance (sync): Used by command-line tools and migrations
Core Configuration
// From auth.ts:53-133
const sharedConfig = {
emailAndPassword: {
enabled: true ,
requireEmailVerification: true ,
async signUpValidator ({ email , password , username , name }) {
validateUserInput ({ email , password , username , name });
},
async sendResetPassword ({ user , url }) {
await smtp . send ({
to: user . email ,
subject: "Password reset - Réinitialisation de votre mot de passe" ,
text: `Cliquez sur le lien suivant pour réinitialiser votre mot de passe : ${ url } ` ,
html: `<p>Cliquez sur le lien suivant pour réinitialiser votre mot de passe :</p><p><a href=" ${ url } "> ${ url } </a></p>` ,
});
},
},
emailVerification: {
async sendVerificationEmail ({ user , url }) {
await smtp . send ({
to: user . email ,
subject: "verify your email address - Vérifiez votre adresse email" ,
text: `Cliquez sur le lien suivant pour vérifier votre adresse email : ${ url } ` ,
html: `<p>Cliquez sur le lien suivant pour vérifier votre adresse email :</p><p><a href=" ${ url } "> ${ url } </a></p>` ,
});
},
sendOnSignUp: true ,
},
session: {
expiresIn: 60 * 60 * 24 * 7 , // 7 days
updateAge: 60 * 60 * 24 , // refresh daily
cookieCache: {
enabled: true ,
strategy: "compact" ,
maxAge: 60 * 5 , // 5 minutes
},
freshAge: 60 * 10 , // 10 minutes
absoluteTimeout: 60 * 60 * 24 * 7 , // 7 days max
},
};
Plugins
Concordia uses three Better Auth plugins to extend functionality.
Username Plugin
Enables username-based authentication in addition to email:
import { username } from "better-auth/plugins/username" ;
plugins : [
username (),
// ...
]
Features :
Allows users to sign in with username or email
Usernames are validated during signup
Unique constraint enforced at database level
Organization Plugin
Provides multi-tenant organization support:
// From auth.ts:158-173
organization ({
ac , // Access control from permissions.ts
roles , // Role definitions (owner, admin, member)
allowUserToCreateOrganization : async () => true ,
async sendInvitationEmail ( data ) {
if ( process . env . SMTP_MOCK === '1' || process . env . NODE_ENV === 'test' ) {
console . log ( '[MOCK SMTP] Invite' , { to: data . email , orgId: data . organization . id });
return ;
}
await smtp . send ({
to: data . email ,
subject: `Invitation à rejoindre ${ data . organization . name } ` ,
text: `Cliquez ici pour rejoindre : ${ process . env . BETTER_AUTH_URL } /invite/ ${ data . id } ` ,
});
},
})
Organization API (from auth.ts:318-334):
instance . organizationApi = {
create : ( payload ) => instance . api . createOrganization ( payload ),
setActive : ( payload ) => instance . api . setActiveOrganization ( payload ),
update : ( payload ) => instance . api . updateOrganization ( payload ),
delete : ( payload ) => instance . api . deleteOrganization ( payload ),
inviteMember : ( payload ) => instance . api . createInvitation ( payload ),
updateMemberRole : ( payload ) => instance . api . updateMemberRole ( payload ),
removeMember : ( payload ) => instance . api . removeMember ( payload ),
leave : ( payload ) => instance . api . leaveOrganization ( payload ),
list : ( payload ) => instance . api . listOrganizations ( payload ),
getFull : ( payload ) => instance . api . getFullOrganization ( payload ),
listMembers : ( payload ) => instance . api . listMembers ( payload ),
listUserInvitations : ( payload ) => instance . api . listUserInvitations ( payload ),
};
Admin Plugin
Enables administrative capabilities:
import { admin } from "better-auth/plugins" ;
plugins : [
admin (),
// ...
]
Admin capabilities :
User impersonation for support
Direct user management
Access to all organization data
Email Verification
Email verification is required for all new accounts.
Verification Flow
User signs up
User submits email, password, and optional username/name
Account created
User account is created with emailVerified: false
Verification email sent
System sends verification email with unique token link
User clicks link
User clicks verification link in email
Email verified
Account is marked as verified, user can fully access the platform
Email template (from auth.ts:78-90):
emailVerification : {
async sendVerificationEmail ({ user , url }) {
await smtp . send ({
to: user . email ,
subject: "verify your email address - Vérifiez votre adresse email" ,
text: `Cliquez sur le lien suivant pour vérifier votre adresse email : ${ url } ` ,
html: `<p>Cliquez sur le lien suivant pour vérifier votre adresse email :</p><p><a href=" ${ url } "> ${ url } </a></p>` ,
});
},
sendOnSignUp : true ,
}
Password Reset
Users can reset their password via email.
Reset Flow
Request reset
User enters email address on forgot password page
Reset email sent
System sends password reset email with secure token
User clicks link
User clicks reset link (valid for limited time)
Set new password
User enters and confirms new password
Password updated
Password is updated, user can log in with new credentials
Reset email template (from auth.ts:65-76):
async sendResetPassword ({ user , url }) {
await smtp . send ({
to: user . email ,
subject: "Password reset - Réinitialisation de votre mot de passe" ,
text: `Cliquez sur le lien suivant pour réinitialiser votre mot de passe : ${ url } ` ,
html: `<p>Cliquez sur le lien suivant pour réinitialiser votre mot de passe :</p><p><a href=" ${ url } "> ${ url } </a></p>` ,
});
}
Session Management
Sessions are managed with secure cookies and automatic refresh.
Session Configuration
// From auth.ts:93-103
session : {
expiresIn : 60 * 60 * 24 * 7 , // 7 days
updateAge : 60 * 60 * 24 , // refresh daily
cookieCache : {
enabled : true ,
strategy : "compact" ,
maxAge : 60 * 5 , // cache for 5 minutes
},
freshAge : 60 * 10 , // consider fresh for 10 minutes
absoluteTimeout : 60 * 60 * 24 * 7 , // max 7 days
}
Session lifecycle :
Sessions expire after 7 days of inactivity
Session tokens are automatically refreshed every 24 hours
Cookie cache reduces database queries
Absolute timeout ensures re-authentication after 7 days
Session Security
// From auth.ts:178-192
advanced : {
useSecureCookies : true ,
disableOriginCheck : false ,
trustedOrigins : [ process . env . BETTER_AUTH_URL ],
cookiePrefix : "astro_" ,
crossSubDomainCookies : { enabled : false },
ipAddress : {
ipAddressHeaders : [ "x-real-ip" , "x-forwarded-for" , "cf-connecting-ip" ],
disableIpTracking : false ,
},
}
Security features :
Secure cookies (HTTPS only in production)
Origin checking enabled
Trusted origins whitelist
IP tracking for audit logs
Bearer token invalidation on logout
Rate Limiting
Built-in rate limiting protects against brute force attacks.
// From auth.ts:105-127
rateLimit : {
enabled : true ,
storage : "database" ,
skipSuccessfulRequests : false ,
skipFailedRequests : false ,
windows : [
{
key: "global_ip" ,
max: 100 ,
window: 60 * 1000 , // 100 requests per minute
},
{
key: "sign_in" ,
max: 5 ,
window: 15 * 60 * 1000 , // 5 attempts per 15 minutes
},
{
key: "sign_up" ,
max: 10 ,
window: 60 * 60 * 1000 , // 10 signups per hour
},
],
}
Rate limit windows :
Global : 100 requests per minute per IP
Sign in : 5 attempts per 15 minutes
Sign up : 10 registrations per hour
Rate limit counters are stored in the database for persistence across restarts.
Post-Signup Hooks
When a user signs up, several actions occur automatically:
// From auth.ts:196-266
databaseHooks : {
user : {
create : {
after : async ( user ) => {
const derivedUsername = user . username ||
user . email . split ( "@" )[ 0 ]. replace ( / [ ^ a-zA-Z0-9- ] / g , "-" ). slice ( 0 , 30 );
// Create profile
await db . profile . insert ({
id: randomUUID (),
userId: user . id ,
username: derivedUsername ,
fullName: user . name || null ,
preferredLanguage: "fr" ,
});
// Create wallet
await db . wallet . insert ({
id: randomUUID (),
userId: user . id ,
balance: "0.00" ,
currency: "EUR" ,
});
// Assign citizen role
await db . userRole . insert ({
id: randomUUID (),
userId: user . id ,
role: "citizen" ,
grantedBy: null ,
});
// Audit log
await db . insert ( auditLog ). values ({
id: randomUUID (),
action: "signup" ,
userId: user . id ,
data: { email: user . email , username: derivedUsername },
});
},
},
},
}
Automatic Profile Creation
Every new user gets a profile with:
Auto-generated username (from email if not provided)
Preferred language set to French (“fr”)
Empty bio and optional full name
Every user receives a digital wallet:
Starting balance: 0.00 EUR
Ready for transactions, bookings, and donations
All users are automatically granted the citizen role:
Enables core platform features
Additional roles can be granted by admins
Signup events are logged:
User ID and email recorded
Timestamp captured
Available for compliance and debugging
Audit Logging
All authentication events are logged to the audit_log table.
Login Success
// From auth.ts:290-306
session : {
create : {
after : async ( session , ctx ) => {
const ip = extractIP ( ctx );
const userAgent = ctx ?. request ?. headers ?. get ( "user-agent" );
await db . insert ( auditLog ). values ({
id: randomUUID (),
action: "login_success" ,
userId: session . userId ,
ip ,
userAgent ,
data: { sessionId: session . id },
});
},
},
}
Login Failure
// From auth.ts:25-50
async function logAuthError ( error , ctx , db ) {
const ip = extractIP ( ctx );
const userAgent = ctx ?. request ?. headers ?. get ( "user-agent" );
let email = "unknown" ;
try {
const body = await ctx ?. request ?. json ?.();
email = body ?. email || "unknown" ;
} catch {}
await db . insert ( auditLog ). values ({
id: randomUUID (),
action: "login_failed" ,
userId: null ,
targetId: email ,
ip ,
userAgent ,
data: { email , error: error . message },
});
}
Logged events :
signup - New user registration
login_success - Successful authentication
login_failed - Failed login attempt (with IP and user agent)
Usage Examples
Get Current Session
import { getAuth } from "@lib/auth/auth" ;
const auth = await getAuth ();
const session = await auth . api . getSession ({ headers: request . headers });
if ( ! session ?. user ?. id ) {
return new Response ( "Unauthorized" , { status: 401 });
}
Sign Up New User
const result = await auth . api . signUpEmail ({
body: {
email: "user@example.com" ,
password: "SecurePass123!" ,
username: "johndoe" ,
name: "John Doe" ,
},
});
Sign In
const session = await auth . api . signInEmail ({
body: {
email: "user@example.com" ,
password: "SecurePass123!" ,
},
});
Request Password Reset
await auth . api . forgotPassword ({
body: {
email: "user@example.com" ,
},
});
Sign Out
await auth . api . signOut ({
headers: request . headers ,
});
Security Best Practices
Always verify email
Email verification is required - do not disable it in production.
Use strong passwords
Password validation ensures minimum complexity requirements.
Monitor rate limits
Check rate limit metrics to detect potential attacks.
Review audit logs
Regularly review audit_log for suspicious activity.
Secure cookies in production
Ensure useSecureCookies: true and HTTPS is enforced.
Environment Variables
Required configuration:
# Better Auth
BETTER_AUTH_URL = https://concordia-annecy.fr
BETTER_AUTH_SECRET = your-secret-key-min-32-chars
# Database
DATABASE_URL = postgresql://user:pass@localhost:5432/concordia
# SMTP (for emails)
SMTP_HOST = smtp.example.com
SMTP_PORT = 587
SMTP_USER = noreply@concordia-annecy.fr
SMTP_PASS = smtp-password
SMTP_FROM = noreply@concordia-annecy.fr
See Also