Waku Integration
Better Auth can be easily integrated with Waku. Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the installation.
Create auth instance
Create a file named auth.ts in your application. Import Better Auth and create your instance.
Make sure to export the auth instance with the variable name auth or as a default export.
import { betterAuth } from "better-auth"
export const auth = betterAuth({
database: {
provider: "postgres", //change this to your database provider
url: process.env.DATABASE_URL, // path to your database or connection string
}
})Create API Route
We need to mount the handler to a API route. Create a directory for Waku's file system router at src/pages/api/auth. Create a catch-all route file [...route].ts inside the src/pages/api/auth directory. And add the following code:
import { auth } from "../../../../auth" // Adjust the path as necessary
export const GET = async (request: Request): Promise<Response> => {
return auth.handler(request)
}
export const POST = async (request: Request): Promise<Response> => {
return auth.handler(request)
}You can change the path on your better-auth configuration but it's recommended to keep it as src/pages/_api/api/auth/[...route].ts
Create a client
Create a client instance. Here we are creating auth-client.ts file inside the lib/ directory.
import { createAuthClient } from "better-auth/react" // make sure to import from better-auth/react
export const authClient = createAuthClient({
//you can pass client configuration here
})
export type Session = typeof authClient.$Infer.Session // you can infer typescript types from the authClientOnce you have created the client, you can use it to sign up, sign in, and perform other actions. Some of the actions are reactive. The client uses nano-store to store the state and re-render the components when the state changes.
The client also uses better-fetch to make the requests. You can pass the fetch configuration to the client.
RSC and Server actions
The api object exported from the auth instance contains all the actions that you can perform on the server. Every endpoint made inside Better Auth is a invocable as a function. Including plugins endpoints.
Example: Getting Session on a server action
"use server" // Waku currently only supports file-level "use server"
import { auth } from "./auth"
import { unstable_getContext as getContext } from "waku/server"
export const someAuthenticatedAction = async () => {
"use server"
const session = await auth.api.getSession({
headers: new Headers(getContext().req.headers),
})
};Example: Getting Session on a RSC
import { auth } from "../auth"
import { unstable_getContext as getContext } from "waku/server"
export async function ServerComponent() {
const session = await auth.api.getSession({
headers: new Headers(getContext().req.headers),
})
if(!session) {
return <div>Not authenticated</div>
}
return (
<div>
<h1>Welcome {session.user.name}</h1>
</div>
)
}Server Action Cookies
When you call a function that needs to set cookies, like signInEmail or signUpEmail in a server action, cookies won’t be set.
We can create a plugin that works together with our middleware to set cookies.
import { betterAuth } from "better-auth";
import { unstable_getContextData as getContextData } from "waku/server"
export const auth = betterAuth({
//...your config
plugins: [wakuCookies()] // make sure this is the last plugin in the array
})
function wakuCookies() {
return {
id: "waku-cookies",
hooks: {
after: [
{
matcher(ctx) {
return true;
},
handler: createAuthMiddleware(async (ctx) => {
const returned = ctx.context.responseHeaders;
if ("_flag" in ctx && ctx._flag === "router") {
return;
}
if (returned instanceof Headers) {
const setCookieHeader = returned?.get("set-cookie");
if (!setCookieHeader) return;
const contextData = getContextData();
contextData.betterAuthSetCookie = setCookieHeader;
}
}),
},
],
},
} satisfies BetterAuthPlugin;
}See below for the middleware to create to add the contextData.betterAuthSetCookie cookies to the response.
Now, when you call functions that set cookies, they will be automatically set.
"use server";
import { auth } from "../auth"
const signIn = async () => {
await auth.api.signInEmail({
body: {
email: "user@email.com",
password: "password",
}
})
}Middleware
In Waku middleware, it's recommended to only check for the existence of a session cookie to handle redirection. This avoids blocking requests by making API or database calls.
You can use the getSessionCookie helper from Better Auth for this purpose:
The getSessionCookie() function does not automatically reference the auth config specified in auth.ts. Therefore, if you customized the cookie name or prefix, you need to ensure that the configuration in getSessionCookie() matches the config defined in your auth.ts.
import type { MiddlewareHandler } from "hono"
import { getSession } from "../auth"
import { getSessionCookie } from "better-auth/cookies"
import type { MiddlewareHandler } from "hono";
import { unstable_getContextData as getContextData } from "waku/server";
const authMiddleware: () => MiddlewareHandler = () => {
return async (c, next) => {
const reqUrl = new URL(c.req.url);
const sessionCookie = getSessionCookie(c.req.raw);
// THIS IS NOT SECURE!
// This is the recommended approach to optimistically redirect users
// We recommend handling auth checks in each page/route
if (
!sessionCookie &&
reqUrl.pathname !== "/" &&
!reqUrl.pathname.startsWith("/api")
) {
if (!reqUrl.pathname.endsWith(".txt")) {
// Currently RSC requests end in .txt and don't handle redirect responses
// The redirect needs to be encoded in the React flight stream somehow
// There is some functionality in Waku to do this from a server component
// but not from middleware.
return c.redirect("/", 302);
}
}
// TODO possible to inspect c.req.url and not do this on every request
// Or skip starting the promise here and just invoke from server components and functions
getSession();
await next();
const contextData = getContextData();
const betterAuthSetCookie = contextData.betterAuthSetCookie as
| string
| undefined;
if (betterAuthSetCookie) {
c.header("set-cookie", betterAuthSetCookie, { append: true });
}
};
};
export default authMiddleware;Security Warning: The getSessionCookie function only checks for the
existence of a session cookie; it does not validate it. Relying solely
on this check for security is dangerous, as anyone can manually create a
cookie to bypass it. You must always validate the session on your server for
any protected actions or pages.
If you have a custom cookie name or prefix, you can pass it to the getSessionCookie function.
const sessionCookie = getSessionCookie(request, {
cookieName: "my_session_cookie",
cookiePrefix: "my_prefix"
})Alternatively, you can use the getCookieCache helper to get the session object from the cookie cache.
import { getCookieCache } from "better-auth/cookies"
const authMiddleware: () => MiddlewareHandler = () => {
return async (c, next) => {
const reqUrl = new URL(c.req.url);
const session = await getCookieCache(c.req.raw)
if (!session && reqUrl.pathname !== "/") {
if (!reqUrl.pathname.endsWith(".txt")) {
ctx.res.status = 302
ctx.res.headers = {
Location: new URL("/", reqUrl).toString(),
}
}
}
}
await next();
}
}
export default authMiddleware;If you place your middleware file in ./src/middleware, it will automatically get loaded by Waku's default server adapter.
How to handle auth checks in each page/route
In this example, we are using the auth.api.getSession function within a server component to get the session object,
then we are checking if the session is valid. If it's not, we are redirecting the user to the sign-in page.
Waku has getContext to get the request headers and getContextData() to store data per request. We can use this
to avoid fetching the session more than once per request.
import { unstable_getContext as getContext, unstable_getContextData as getContextData } from "waku/server";
// Code from above to create the server auth config
// export const auth = ...
export function getSession(): Promise<Session | null> {
const contextData = getContextData();
const ctx = getContext();
const existingSessionPromise = contextData.sessionPromise as
| Promise<Session | null>
| undefined;
if (existingSessionPromise) {
return existingSessionPromise;
}
const sessionPromise = auth.api.getSession({
headers: new Headers(ctx.req.headers),
});
contextData.sessionPromise = sessionPromise;
return sessionPromise;
}import { getSession } from "../auth";
import { unstable_redirect as redirect } from 'waku/router/server';
export default async function DashboardPage() {
const session = await getSession()
if (!session) {
redirect("/sign-in")
}
return (
<div>
<h1>Welcome {session.user.name}</h1>
</div>
)
}Example usage
Sign Up
"use client"
import { useState } from "react"
import { authClient } from "../lib/auth-client"
export default function SignUp() {
const [email, setEmail] = useState("")
const [name, setName] = useState("")
const [password, setPassword] = useState("")
const signUp = async () => {
await authClient.signUp.email(
{
email,
password,
name,
},
{
onRequest: (ctx) => {
// show loading state
},
onSuccess: (ctx) => {
// redirect to home
},
onError: (ctx) => {
alert(ctx.error)
},
},
)
}
return (
<div>
<h2>
Sign Up
</h2>
<form
onSubmit={signUp}
>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button
type="submit"
>
Sign Up
</button>
</form>
</div>
)
}Sign In
"use client"
import { useState } from "react"
import { authClient } from "../lib/auth-client"
export default function SignIn() {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const signIn = async () => {
await authClient.signIn.email(
{
email,
password,
},
{
onRequest: (ctx) => {
// show loading state
},
onSuccess: (ctx) => {
// redirect to home
},
onError: (ctx) => {
alert(ctx.error)
},
},
)
}
return (
<div>
<h2>
Sign In
</h2>
<form onSubmit={signIn}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
type="submit"
>
Sign In
</button>
</form>
</div>
)
}