docs/content/docs/integrations/waku.mdx
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 a file named auth.ts in your application. Import Better Auth and create your instance.
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
}
})
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)
}
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 authClient
Once 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.
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 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_getHeaders as getHeaders } from "waku/server"
export const someAuthenticatedAction = async () => {
"use server"
const headers = getHeaders()
const session = await auth.api.getSession({
headers,
})
};
Example: Getting Session on a RSC
import { auth } from "../auth"
import { unstable_getHeaders as getHeaders } from "waku/server"
export async function ServerComponent() {
const headers = getHeaders()
const session = await auth.api.getSession({
headers,
})
if(!session) {
return <div>Not authenticated</div>
}
return (
<div>
<h1>Welcome {session.user.name}</h1>
</div>
)
}
<Callout type="warn">RSCs that run after the response has started streaming cannot set cookies. The cookie cache will not be refreshed until the server is interacted with from the client via Server Actions or Route Handlers.</Callout>
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 { createAuthMiddleware } from "better-auth/api";
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 // [!code highlight]
})
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: "[email protected]",
password: "password",
}
})
}
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:
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;
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.
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>
)
}
"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>
)
}
"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>
)
}