Skip to main content

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:
  1. API Instance (async): Used by the Astro application
  2. 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

1

User signs up

User submits email, password, and optional username/name
2

Account created

User account is created with emailVerified: false
3

Verification email sent

System sends verification email with unique token link
4

User clicks link

User clicks verification link in email
5

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

1

Request reset

User enters email address on forgot password page
2

Reset email sent

System sends password reset email with secure token
3

User clicks link

User clicks reset link (valid for limited time)
4

Set new password

User enters and confirms new password
5

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 },
        });
      },
    },
  },
}
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

1

Always verify email

Email verification is required - do not disable it in production.
2

Use strong passwords

Password validation ensures minimum complexity requirements.
3

Monitor rate limits

Check rate limit metrics to detect potential attacks.
4

Review audit logs

Regularly review audit_log for suspicious activity.
5

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

Build docs developers (and LLMs) love