Skip to main content
The Authentication API provides user authentication and preferences synchronization using BetterAuth. Authentication features are available when AUTH_ENABLED=true.
Authentication is optional. Planning data is accessible without authentication. Auth enables user preferences sync across devices.

Authentication methods

OAuth providers

Sign in with Discord or GitHub

Passkeys

WebAuthn/FIDO2 passwordless authentication

BetterAuth endpoints

All authentication endpoints are handled by BetterAuth at /api/auth/*:

Session management

GET /api/auth/session
endpoint
Get current user session.Response:
{
  "user": {
    "id": "user-123",
    "email": "[email protected]",
    "name": "John Doe",
    "image": "https://...",
    "theme": "dark",
    "highlightTeacher": true,
    "showWeekends": false,
    "mergeDuplicates": true,
    "blocklist": ["exam", "test"],
    "plannings": ["enscr.elevesing1iereannee"],
    "customGroups": "[...]",
    "colors": "{...}",
    "prefsMeta": "{...}"
  },
  "session": {
    "id": "session-456",
    "userId": "user-123",
    "expiresAt": "2026-04-01T00:00:00.000Z"
  }
}
POST /api/auth/sign-out
endpoint
Sign out the current user.Invalidates the session and clears cookies.

OAuth sign-in

GET /api/auth/sign-in/discord
endpoint
Initiate Discord OAuth flow.Redirects to Discord authorization page.
GET /api/auth/sign-in/github
endpoint
Initiate GitHub OAuth flow.Redirects to GitHub authorization page.
GET /api/auth/callback/discord
endpoint
OAuth callback for Discord.Handles authorization code exchange and user creation.
GET /api/auth/callback/github
endpoint
OAuth callback for GitHub.Handles authorization code exchange and user creation.

Passkey authentication

POST /api/auth/passkey/register
endpoint
Register a new passkey.Request body:
{
  "email": "[email protected]"
}
Initiates WebAuthn registration ceremony.
POST /api/auth/passkey/authenticate
endpoint
Authenticate with passkey.Initiates WebAuthn authentication ceremony.

Custom auth routes

PlanningSup adds custom routes for Tauri/extension OAuth handling:
GET /api/auth/auto-redirect/:provider
endpoint
Auto-redirect for OAuth callbacks in Tauri/extension.Path parameters:
  • provider - OAuth provider (e.g., discord, github)
Query parameters:
  • client - Client type (tauri or extension)
  • Standard OAuth parameters (code, state, etc.)
This endpoint detects Tauri/extension clients and redirects to the appropriate deep link:
  • Tauri: tauri://localhost/auth-callback/discord?...
  • Extension: planningsup://auth-callback/discord?...

User preferences

Authenticated users can sync preferences across devices.

Preference fields

These fields are stored in the user table and synced automatically:
theme
string
UI theme preference.Values: dark, light, dracula, autoDefault: auto
highlightTeacher
boolean
Whether to highlight events missing teacher info.Default: false
showWeekends
boolean
Whether to show weekends in calendar view.Default: true
mergeDuplicates
boolean
Whether to merge duplicate events.Default: false
blocklist
array
Array of keywords to filter out from events.Example: ["exam", "test", "contrôle"]
plannings
array
Array of planning full IDs user has selected.Example: ["enscr.elevesing1iereannee", "fac-de-sciences.l1info"]
customGroups
string
JSON string of custom planning groups.Structure:
Array<{
  id: string
  name: string
  plannings: string[]  // Array of fullIds
}>
Example:
"[{\"id\":\"my-group\",\"name\":\"My Classes\",\"plannings\":[\"enscr.elevesing1iereannee\"]}]"
colors
string
JSON string of custom color mappings.Structure:
Record<string, string>  // planningFullId or category -> color
Example:
"{\"enscr.elevesing1iereannee\":\"#3b82f6\",\"lecture\":\"#10b981\"}"
prefsMeta
string
JSON string of preference update timestamps.Used for conflict resolution during sync.Structure:
Record<string, number>  // preference key -> timestamp
Example:
"{\"theme\":1709419845000,\"blocklist\":1709419850000}"

Preferences sync

The web app uses useUserPrefsSync composable for bidirectional sync:

Sync logic

1

On app load

Fetch user session:
const session = await client.api.auth.session.get()
2

Merge with local state

For each preference:
  1. Compare prefsMeta timestamps
  2. If server value is newer, use it
  3. If local value is newer, push to server
3

Watch for changes

When user updates a preference:
  1. Update local state immediately
  2. Debounce API call (500ms)
  3. Push change to server
  4. Update prefsMeta timestamp

Example: Update preference

import { useUserPrefsSync } from '@/composables/useUserPrefsSync'

const { prefs, updatePref } = useUserPrefsSync()

// Update theme
await updatePref('theme', 'dark')

// Update blocklist
await updatePref('blocklist', ['exam', 'test', 'contrôle'])

// Update custom groups
await updatePref('customGroups', JSON.stringify([
  {
    id: 'my-group',
    name: 'My Classes',
    plannings: ['enscr.elevesing1iereannee']
  }
]))
Preferences are automatically synced to the server and persisted.

Rate limiting

BetterAuth applies rate limits to prevent abuse:
  • General endpoints: 100 requests per 60 seconds
  • Passkey endpoints: 5 requests per 10 seconds
Response when rate limited:
{
  "error": "Too many requests"
}
Status: 429 Too Many Requests The response may include a Retry-After header with seconds to wait.

OAuth configuration

To enable OAuth providers, configure environment variables:

Discord

  1. Create an application at https://discord.com/developers/applications
  2. Add OAuth2 redirect URL: https://your-domain.com/api/auth/callback/discord
  3. Configure environment variables:
AUTH_ENABLED=true
DISCORD_CLIENT_ID=your_discord_client_id
DISCORD_CLIENT_SECRET=your_discord_client_secret
BETTER_AUTH_SECRET=random_secret_key
BETTER_AUTH_URL=https://your-domain.com
PUBLIC_ORIGIN=https://your-domain.com

GitHub

  1. Create an OAuth app at https://github.com/settings/developers
  2. Set callback URL: https://your-domain.com/api/auth/callback/github
  3. Configure environment variables:
AUTH_ENABLED=true
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
BETTER_AUTH_SECRET=random_secret_key
BETTER_AUTH_URL=https://your-domain.com
PUBLIC_ORIGIN=https://your-domain.com

Passkey configuration

Passkeys (WebAuthn) require HTTPS in production:
  1. Ensure PUBLIC_ORIGIN uses HTTPS
  2. Configure TRUSTED_ORIGINS to include all valid origins
  3. Passkeys are registered per-domain (e.g., planningsup.app)
Development note:
  • Localhost is allowed for testing: http://localhost:4444
  • Passkeys registered on localhost won’t work on production domain

Security considerations

Important security practices:
  • Always use HTTPS in production
  • Keep BETTER_AUTH_SECRET secret and rotate regularly
  • Use strong OAuth client secrets
  • Configure TRUSTED_ORIGINS restrictively
  • Never commit secrets to version control
  • Use rate limiting to prevent abuse

Session lifetime

Sessions are configured with:
  • Expires in: 30 days
  • Update age: 7 days (session refreshed every 7 days of activity)
Sessions are stored server-side in PostgreSQL and referenced via HTTP-only cookies.

Example: Full authentication flow

1

User clicks 'Sign in with Discord'

window.location.href = '/api/auth/sign-in/discord'
2

User authorizes on Discord

Discord redirects back to /api/auth/callback/discord?code=...
3

Server exchanges code for tokens

BetterAuth:
  1. Exchanges authorization code for access token
  2. Fetches user profile from Discord
  3. Creates or updates user in database
  4. Creates session and sets cookie
4

User redirected to app

App checks session:
const session = await client.api.auth.session.get()
if (session.data?.user) {
  // User is authenticated
  console.log('Logged in as:', session.data.user.email)
}
5

Preferences sync automatically

useUserPrefsSync merges local and server preferences based on timestamps.

Next steps

Environment variables

Configure authentication environment variables

Architecture

Learn about the authentication system design

Build docs developers (and LLMs) love