Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Greens-Organization/pz-packs/llms.txt

Use this file to discover all available pages before exploring further.

Overview

PZ Packs uses Better Auth with Discord OAuth2 for secure, passwordless authentication. Instead of managing separate credentials, you authenticate using your existing Discord account, which provides a seamless and secure login experience.
PZ Packs only requests basic Discord profile information (username, avatar, and user ID). We do not access your Discord messages, servers, or any sensitive data.

Authentication Architecture

Technology Stack

The authentication system is built on Better Auth, a modern authentication framework for TypeScript applications:
// packages/auth/auth.ts - Server-side auth configuration
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'

export const auth = betterAuth({
  basePath: '/auth',
  database: drizzleAdapter(database, { 
    provider: 'pg', 
    usePlural: true 
  }),
  trustedOrigins: env.ORIGIN_ALLOWED,
  session: { 
    cookieCache: { 
      enabled: true, 
      maxAge: 60 * 5  // 5 minute cookie cache
    } 
  },
  socialProviders: {
    discord: {
      clientId: env.DISCORD_CLIENT_ID,
      clientSecret: env.DISCORD_CLIENT_SECRET,
    },
  },
})

Client-Side Integration

The web application uses the Better Auth React client for seamless authentication hooks:
// apps/web/src/lib/auth.ts
import { generateAuthClient } from '@org/auth/client'

export const authClient = generateAuthClient(env.VITE_API_URL)

// Usage in React components
function Header() {
  const { data: session, isPending } = authClient.useSession()
  
  if (!session) {
    return (
      <Button onClick={() => 
        authClient.signIn.social({
          provider: 'discord',
          callbackURL: location.origin,
        })
      }>
        Sign In
      </Button>
    )
  }
  
  return <UserProfile user={session.user} />
}

OAuth2 Flow

1

Initiate Sign In

When you click “Sign In” on the PZ Packs website, the client initiates an OAuth2 flow with Discord:
authClient.signIn.social({
  provider: 'discord',
  callbackURL: location.origin,
})
This redirects you to Discord’s authorization page at:
https://discord.com/oauth2/authorize?
  client_id={DISCORD_CLIENT_ID}&
  redirect_uri={CALLBACK_URL}&
  response_type=code&
  scope=identify
2

Discord Authorization

Discord presents an authorization screen showing:
  • PZ Packs application requesting access
  • Requested permissions: Read your username and avatar
  • Options to Authorize or Cancel
When you click “Authorize”, Discord validates your credentials and generates an authorization code.
3

Token Exchange

Discord redirects back to PZ Packs with an authorization code:
https://pzpacks.grngroup.net/auth/callback/discord?code={AUTH_CODE}
The PZ Packs backend exchanges this code for access tokens:
// Better Auth handles this automatically
POST https://discord.com/api/oauth2/token
{
  client_id: DISCORD_CLIENT_ID,
  client_secret: DISCORD_CLIENT_SECRET,
  grant_type: 'authorization_code',
  code: AUTH_CODE,
  redirect_uri: CALLBACK_URL
}

// Response from Discord:
{
  access_token: "...",
  token_type: "Bearer",
  expires_in: 604800,
  refresh_token: "...",
  scope: "identify"
}
4

Fetch User Profile

Using the access token, PZ Packs fetches your Discord profile:
GET https://discord.com/api/users/@me
Authorization: Bearer {ACCESS_TOKEN}

// Response:
{
  id: "573948573829403640",
  username: "survivor_123",
  avatar: "a_d5efa99b3eczf9dfbkjh",
  discriminator: "0",  // Legacy, now always "0"
  global_name: "Zombie Slayer"
}
5

Create or Update User

PZ Packs stores your profile in the database:
// Database schema: packages/database/schemas/users.ts
interface User {
  id: string              // UUID generated by PZ Packs
  name: string            // Discord username
  email: string | null    // Not collected from Discord
  image: string | null    // Discord avatar URL
  role: 'user' | 'admin'  // Default: 'user'
  createdAt: Date
  updatedAt: Date
}

// Account linking table
interface Account {
  id: string
  userId: string          // References User.id
  accountId: string       // Discord user ID
  providerId: 'discord'
  accessToken: string     // Encrypted
  refreshToken: string    // Encrypted
  expiresAt: Date
}
6

Session Creation

A secure session is created and stored in Redis cache:
// Session structure
interface Session {
  id: string
  userId: string
  expiresAt: Date
  ipAddress: string
  userAgent: string
}

// Session cookie is set (HTTP-only, Secure, SameSite=Lax)
Set-Cookie: better-auth.session_token={SESSION_TOKEN}; 
            HttpOnly; 
            Secure; 
            SameSite=Lax; 
            Max-Age=2592000
The session is cached in Redis with a 5-minute TTL for faster lookups:
session: { 
  cookieCache: { 
    enabled: true, 
    maxAge: 60 * 5  // 5 minutes
  } 
}

Session Management

Session Lifecycle

Sessions in PZ Packs are designed for security and performance:
  • Duration: Sessions last for 30 days (2,592,000 seconds)
  • Cookie Cache: 5-minute cache in Redis for fast authentication checks
  • Storage: Primary session data in PostgreSQL, cached copy in Redis
  • HTTP-Only Cookies: Session tokens cannot be accessed via JavaScript (XSS protection)
  • Secure Flag: Cookies only transmitted over HTTPS in production
  • SameSite: Lax setting prevents CSRF attacks while allowing OAuth redirects

Checking Authentication Status

In your React components, use the useSession hook:
import { authClient } from '@/lib/auth'

function ProtectedComponent() {
  const { data: session, isPending, error } = authClient.useSession()
  
  if (isPending) {
    return <LoadingSpinner />
  }
  
  if (!session) {
    return <SignInPrompt />
  }
  
  return (
    <div>
      <h1>Welcome, {session.user.name}!</h1>
      <Avatar src={session.user.image} />
    </div>
  )
}

Signing Out

To end a session, use the signOut method:
<Button onClick={() => authClient.signOut()}>
  Log Out
</Button>
This will:
  1. Delete the session from the database
  2. Clear the session cache in Redis
  3. Remove the session cookie from the browser
  4. Redirect you to the homepage

API Authentication

Authenticated Requests

When making API requests, the session cookie is automatically included:
// Frontend request (cookie sent automatically)
const response = await fetch('https://pzpacks.grngroup.net/api/modpacks', {
  credentials: 'include',  // Important: Include cookies
  headers: {
    'Content-Type': 'application/json',
  },
})

Backend Authentication Middleware

The API uses Better Auth macros to protect endpoints:
// apps/api/src/infra/http/plugins/better-auth.ts
export const betterAuthPlugin = new Elysia({ name: 'better-auth' })
  .mount(auth.handler)
  .macro({
    auth: {
      async resolve({ status, request: { headers } }) {
        const session = await auth.api.getSession({ headers })
        
        if (!session) {
          return status(401, { message: 'Unauthorized' })
        }
        
        return session
      },
    },
  })

// Usage in routes
server.post('/api/modpacks', async ({ body, auth }) => {
  // auth contains the session and user data
  const modpack = await createModpack({
    ...body,
    owner: auth.user.id,
  })
  
  return modpack
}, { auth: true })  // Requires authentication

Permission-Based Authorization

PZ Packs implements role-based access control (RBAC):
// packages/auth/permissions/index.ts
const statement = {
  modpack: ['create', 'read', 'update', 'archive', 'export', 
            'add-mod', 'remove-mod', 'import', 'manager-members'],
  mod: ['create', 'read', 'update', 'remove', 'update-all'],
  notification: ['read', 'update'],
  admin: ['access'],
}

// User role (default)
const user = ac.newRole({
  modpack: ['create', 'read', 'update', 'archive', 'export', 
            'add-mod', 'remove-mod', 'import', 'manager-members'],
  mod: ['create', 'read', 'update', 'remove'],
  notification: ['read', 'update'],
})

// Admin role (elevated permissions)
const admin = ac.newRole({
  modpack: [...statement.modpack],
  mod: [...statement.mod],
  notification: [...statement.notification],
  admin: ['access'],  // Can access admin panel
})
Protected routes check permissions:
server.patch('/api/mods/:id/update-all', async ({ params, auth }) => {
  // Only admins can update all mods
  // Permission check happens via Better Auth plugin
  return await updateAllMods(params.id)
}, { 
  permission: { 
    resource: 'mod', 
    action: 'update-all' 
  } 
})

Environment Configuration

Required Environment Variables

To set up authentication in your own deployment:
# packages/auth/.env.example

# Discord OAuth Application Credentials
DISCORD_CLIENT_ID="573984042616261408"
DISCORD_CLIENT_SECRET="pUdpxAQfWTMKuo_ZWWjxndMjcVan5VRc"

# Allowed Origins (CORS)
ORIGIN_ALLOWED="http://localhost:3000,http://localhost:4173"

# Database Connection
DATABASE_URL="postgres://user:pass@localhost:54320/pz_packs"

Creating a Discord OAuth Application

To create your own Discord OAuth app:
1

Visit Discord Developer Portal

Navigate to Discord Developer Portal and click “New Application”.
2

Configure OAuth2 Settings

In your application settings:
  1. Go to OAuth2General
  2. Add your redirect URIs:
    • Development: http://localhost:3000/auth/callback/discord
    • Production: https://yourdomain.com/auth/callback/discord
  3. Copy your Client ID and Client Secret
3

Set Required Scopes

PZ Packs only requires the identify scope to read basic profile information (username and avatar).
4

Update Environment Variables

Add the credentials to your .env file:
DISCORD_CLIENT_ID="your_client_id"
DISCORD_CLIENT_SECRET="your_client_secret"

CORS Configuration

The API must allow requests from your frontend origin:
// apps/api/src/infra/http/plugins/cors.ts
import cors from '@elysiajs/cors'

export const corsPlugin = cors({
  origin: env.ORIGIN_ALLOWED,  // Array of allowed origins
  credentials: true,            // Allow cookies
  methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
})
Important: ORIGIN_ALLOWED must include your frontend URL. Without proper CORS configuration, authentication cookies will not be sent with API requests, causing authentication to fail.

Security Considerations

Best Practices

HTTPS Only in Production

Session cookies have the Secure flag in production, requiring HTTPS. Never deploy without SSL/TLS certificates.

HTTP-Only Cookies

Session tokens are stored in HTTP-only cookies, making them inaccessible to JavaScript and preventing XSS attacks.

SameSite Protection

SameSite=Lax prevents CSRF attacks by restricting cookie transmission to same-site requests and safe cross-site navigation.

Token Encryption

OAuth tokens (access and refresh tokens) are encrypted before storage in the database using Better Auth’s built-in encryption.

Session Security Features

// Session validation includes:
// 1. Token signature verification
// 2. Expiration check
// 3. IP address validation (optional)
// 4. User agent validation (optional)

const session = await auth.api.getSession({ headers })

if (!session || session.expiresAt < new Date()) {
  return status(401, { message: 'Session expired' })
}

Rate Limiting

API endpoints are protected by rate limiting to prevent abuse:
// Example rate limit configuration
{
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 100,                  // 100 requests per window
  message: 'Too many requests, please try again later',
}

Troubleshooting

Possible causes:
  • Session cookie not being sent with requests (check credentials: 'include')
  • Session expired (sessions last 30 days)
  • CORS misconfiguration preventing cookies
  • Browser blocking third-party cookies
Solution:
// Ensure fetch includes credentials
fetch('/api/modpacks', {
  credentials: 'include',  // Critical for cookies
})

// Verify CORS allows your origin
console.log(response.headers.get('Access-Control-Allow-Origin'))
Possible causes:
  • Callback URL mismatch in Discord app settings
  • Session not being created after OAuth callback
  • Browser blocking cookies
Solution:
  1. Verify Discord app redirect URI matches exactly: https://yourdomain.com/auth/callback/discord
  2. Check browser console for cookie errors
  3. Ensure ORIGIN_ALLOWED includes your frontend URL
  4. Clear browser cookies and try again
Possible causes:
  • Session cookie not persisted (check cookie settings)
  • Redis cache cleared but PostgreSQL still has session
  • Browser privacy settings blocking persistent cookies
Solution:
  • Check cookie Max-Age is set (30 days = 2,592,000 seconds)
  • Verify HttpOnly and Secure flags are appropriate for environment
  • Test in incognito mode to rule out browser extensions
Possible causes:
  • Insufficient permissions for your role
  • Permission middleware not properly configured
  • Session exists but user role not loaded
Solution:
// Check your user role
const { data: session } = authClient.useSession()
console.log('User role:', session?.user?.role)

// Verify endpoint requires correct permission
// Admin-only routes require 'admin' role

Advanced Topics

Token Refresh

Better Auth automatically handles token refresh for Discord OAuth:
// Refresh happens automatically when access token expires
// Refresh tokens are valid for 30 days from Discord
// Better Auth refreshes tokens 24 hours before expiration

Multiple Sessions

Users can have multiple active sessions (e.g., desktop and mobile):
// List all user sessions
GET /api/auth/sessions

// Revoke a specific session
DELETE /api/auth/sessions/{sessionId}

// Revoke all sessions (except current)
DELETE /api/auth/sessions/all

Custom User Fields

The user schema includes a role field with additional metadata support:
// packages/auth/auth.ts
user: {
  additionalFields: {
    role: {
      type: 'string',
      defaultValue: 'user',
    },
  },
}

// Access in application
const { user } = session
console.log(user.role)  // 'user' or 'admin'

Better Auth Documentation

Learn more about the authentication framework powering PZ Packs

Discord OAuth2 Documentation

Official Discord OAuth2 implementation guide

Quick Start Guide

Get started with PZ Packs in under 5 minutes

API Reference

Explore authenticated API endpoints
For security concerns or authentication issues, please contact us on Discord or open an issue on GitHub.