Straktur
Authentication

Authentication Setup

Configure authentication methods - Email/Password, Magic Link, Google, GitHub, and more

TL;DR: Email/Password works out of the box. Enable social logins in appConfig.

Straktur uses Better Auth for authentication - a modern, type-safe auth library.

Available Methods

MethodDefaultSetup Required
Email/Password✅ EnabledNone
Password Reset✅ EnabledEmail provider
Magic LinkDisabledEmail provider
GoogleDisabledOAuth credentials
GitHubDisabledOAuth credentials
DiscordDisabledOAuth credentials
AppleDisabledOAuth credentials

Configuration

All auth methods are configured in src/lib/config/index.ts:

export const appConfig = {
  auth: {
    methods: {
      emailPassword: true,      // ✅ Default
      passwordReset: true,      // ✅ Default
      magicLink: false,         // Enable below
      google: false,            // Enable below
      github: false,            // Enable below
      discord: false,           // Enable below
      apple: false,             // Enable below
    }
  }
}

Passwordless login via email link.

Setup

  1. Configure an email provider (Resend, SMTP, or Mailpit in Docker)

  2. Enable in config:

auth: {
  methods: {
    magicLink: true
  }
}
  1. Restart the dev server (or recreate the Docker app container) so the Better Auth plugin loads.

Users can sign in from /login using the Magic link tab. The link expires after 10 minutes.

How it works

  • Server: magicLink plugin in src/lib/auth/providers/better-auth/instance.ts sends a templated email via sendTemplatedEmail()
  • Client: signInWithMagicLink() in src/lib/auth/providers/better-auth/client.ts
  • UI: MagicLinkForm on /login (Password | Magic link tabs when both methods are enabled)
  • Email template: src/emails/magic-link.tsx with translations in messages/email/

New users can sign up via magic link unless signupMode is invite_only (same behavior as OAuth).

Local testing with Docker

With Mailpit (EMAIL_PROVIDER=smtp, SMTP_HOST=mailpit):

  1. Set magicLink: true in appConfig
  2. Run docker compose up
  3. Open http://localhost:8080 (Mailpit UI)
  4. Request a magic link on /login, then click the link in Mailpit

Google OAuth

Setup

  1. Go to Google Cloud Console

  2. Create project → APIs & Services → Credentials

  3. Create OAuth 2.0 Client ID (Web application)

  4. Add authorized redirect URI:

    http://localhost:3000/api/auth/callback/google
    https://yourdomain.com/api/auth/callback/google
  5. Add environment variables:

NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-your-secret
  1. Enable in config:
auth: {
  methods: {
    google: true
  }
}

Social login buttons appear on /login and /register when both the config flag and env vars are set.


GitHub OAuth

Setup

  1. Go to GitHub Developer Settings

  2. OAuth Apps → New OAuth App

  3. Set callback URL:

    http://localhost:3000/api/auth/callback/github
  4. Add environment variables:

NEXT_PUBLIC_GITHUB_CLIENT_ID=your-client-id
GITHUB_CLIENT_SECRET=your-client-secret
  1. Enable in config:
auth: {
  methods: {
    github: true
  }
}

Social login buttons appear on /login and /register automatically when both the config flag and env vars are set.


Discord OAuth

Setup

  1. Go to Discord Developer Portal

  2. Create application → OAuth2 → General

  3. Add redirect:

    http://localhost:3000/api/auth/callback/discord
  4. Add environment variables:

NEXT_PUBLIC_DISCORD_CLIENT_ID=your-client-id
DISCORD_CLIENT_SECRET=your-client-secret
  1. Enable in config:
auth: {
  methods: {
    discord: true
  }
}

Apple OAuth

Setup

  1. Go to Apple Developer

  2. Create App ID with Sign in with Apple

  3. Create Service ID with callback:

    https://yourdomain.com/api/auth/callback/apple
  4. Add environment variables:

NEXT_PUBLIC_APPLE_CLIENT_ID=your-service-id
APPLE_CLIENT_SECRET=your-generated-secret
  1. Enable in config:
auth: {
  methods: {
    apple: true
  }
}

Apple Sign In requires HTTPS and a verified domain. It won't work on localhost.


Required Environment Variables

VariableRequiredDescription
BETTER_AUTH_SECRETYesSession encryption (min 32 chars)
NEXT_PUBLIC_APP_URLYesYour app URL for callbacks
NEXT_PUBLIC_GOOGLE_CLIENT_IDOAuthGoogle OAuth client ID (public)
GOOGLE_CLIENT_SECRETOAuthGoogle OAuth client secret
NEXT_PUBLIC_GITHUB_CLIENT_IDOAuthGitHub OAuth client ID (public)
GITHUB_CLIENT_SECRETOAuthGitHub OAuth client secret

Docker Compose

When using docker compose up, copy .env.example to .env, add OAuth credentials, and enable providers in appConfig. The app service reads OAuth vars from .env. After changing OAuth env vars, recreate the app container:

docker compose up --force-recreate app

Generate Secret

openssl rand -base64 32

Password Resets

Two flows are supported: a user-initiated reset from the login page, and an admin-triggered reset from the Users UI. Both require an email provider — see Email Setup.

User-initiated

A "Forgot password?" link on the login page sends a signed reset email to the user. They click the link, land on /reset-password?token=..., and pick a new password. Enabled by default via auth.methods.passwordReset: true — no extra wiring.

Admin-triggered

Admins can send a password reset email for any team member from the Users list. Useful for onboarding, locked-out users, or before SSO is in place.

  • UI: Users list → row actions → "Send password reset"
  • Router: orpcUtils.users.triggerPasswordReset — requires admin role or higher (adminProcedure)
  • What the user receives: the same reset email as the self-service flow — single-use token, valid for 1 hour
import { orpcUtils } from "@/lib/orpc/client"

const resetPassword = useMutation({
  ...orpcUtils.users.triggerPasswordReset.mutationOptions(),
  onSuccess: () => toast({ title: "Password reset email sent" }),
})

<Button onClick={() => resetPassword.mutate({ userId: user.id })}>
  Send password reset
</Button>

The send call is await-ed on the server — fire-and-forget was silently dropping emails on serverless runtimes. If you wire up a custom password-reset flow, keep the await.


Sign-up Modes

Control how new users join your app in appConfig:

auth: {
  signupMode: "join_default_org_pending_approval"
}
ModeBehavior
openAnyone can sign up, immediately active
join_default_org_pending_approvalSign up, but admin must approve
invite_onlyOnly invited users can join

Roles & Permissions

Once a user is signed in, their permissions are determined by their role in the active organization: owner > admin > member > viewer.

See Roles & Permissions (RBAC) for role levels, server-side checks with hasMinRole(), client-side access via useSessionRole(), and hiding nav items with minRole.


Common Issues

On this page