Skip to content

Better Auth

Better Auth is framework-agnostic authentication (and authorization) framework for TypeScript.

It provides a comprehensive set of features out of the box and includes a plugin ecosystem that simplifies adding advanced functionalities.

We recommended going through Better Auth basic setup before going through this page.

Our basic setup will look like this:

ts
import { betterAuth } from 'better-auth'
import { Pool } from 'pg'

export const auth = betterAuth({
    database: new Pool()
})

Handler

After setting up Better Auth instance, we can mount to Elysia via mount.

We need to mount the handler to Elysia endpoint.

ts
import { Elysia } from 'elysia'
import { auth } from './auth'

const app = new Elysia()
	.mount(auth.handler) 
	.listen(3000)

console.log(
    `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)

Then we can access Better Auth with http://localhost:3000/api/auth.

Custom endpoint

We recommended setting a prefix path for when using mount.

ts
import { Elysia } from 'elysia'

const app = new Elysia()
	.mount('/auth', auth.handler) 
	.listen(3000)

console.log(
    `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)

Then we can access Better Auth with http://localhost:3000/auth/api/auth.

But the URL looks redundant, we can customize the /api/auth prefix to something else in Better Auth instance.

ts
import { betterAuth } from 'better-auth'
import { openAPI } from 'better-auth/plugins'
import { passkey } from 'better-auth/plugins/passkey'

import { Pool } from 'pg'

export const auth = betterAuth({
    basePath: '/api'
})

Then we can access Better Auth with http://localhost:3000/auth/api.

Unfortunately, we can't set basePath of a Better Auth instance to be empty or /.

Swagger / OpenAPI

Better Auth support openapi with better-auth/plugins.

However if we are using @elysiajs/swagger, you might want to extract the documentation from Better Auth instance.

We may do that with the following code:

ts
import { openAPI } from 'better-auth/plugins'

let _schema: ReturnType<typeof auth.api.generateOpenAPISchema>
const getSchema = async () => (_schema ??= auth.api.generateOpenAPISchema())

export const OpenAPI = {
    getPaths: (prefix = '/auth/api') =>
        getSchema().then(({ paths }) => {
            const reference: typeof paths = Object.create(null)

            for (const path of Object.keys(paths)) {
                const key = prefix + path
                reference[key] = paths[path]

                for (const method of Object.keys(paths[path])) {
                    const operation = (reference[key] as any)[method]

                    operation.tags = ['Better Auth']
                }
            }

            return reference
        }) as Promise<any>,
    components: getSchema().then(({ components }) => components) as Promise<any>
} as const

Then in our Elysia instance that use @elysiajs/swagger.

ts
import { Elysia } from 'elysia'
import { swagger } from '@elysiajs/swagger'

import { OpenAPI } from './auth'

const app = new Elysia().use(
    swagger({
        documentation: {
            components: await OpenAPI.components,
            paths: await OpenAPI.getPaths()
        }
    })
)

CORS

To configure cors, you can use the cors plugin from @elysiajs/cors.

ts
import { Elysia } from 'elysia'
import { cors } from '@elysiajs/cors'

import { auth } from './auth'

const app = new Elysia()
    .use(
        cors({
            origin: 'http://localhost:3001',
            methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
            credentials: true,
            allowedHeaders: ['Content-Type', 'Authorization']
        })
    )
    .mount(auth.handler)
    .listen(3000)

console.log(
    `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)

Macro

You can use macro with resolve to provide session and user information before pass to view.

ts
import { Elysia } from 'elysia'
import { auth } from './auth'

// user middleware (compute user and session and pass to routes)
const betterAuth = 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)

                return {
                    user: session.user,
                    session: session.session
                }
            }
        }
    })

const app = new Elysia()
    .use(betterAuth)
    .get('/user', ({ user }) => user, {
        auth: true
    })
    .listen(3000)

console.log(
    `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
)

This will allow you to access the user and session object in all of your routes.