Better Auth provides a comprehensive authentication solution that integrates seamlessly with Rivet Actors using the onAuth
hook.
View Example on GitHub Check out the complete example
Installation
Install Better Auth alongside RivetKit:
npm install better-auth better-sqlite3
npm install -D @types/better-sqlite3
# For React integration
npm install @rivetkit/react
Backend Setup
Configure Better Auth
Create your authentication configuration:
import { betterAuth } from "better-auth" ;
import Database from "better-sqlite3" ;
export const auth = betterAuth ({
database: new Database ( "/tmp/auth.sqlite" ),
trustedOrigins: [ "http://localhost:5173" ],
emailAndPassword: {
enabled: true ,
},
});
Generate & Run Migrations
Create and apply the database schema:
# Generate migration files
pnpm dlx @better-auth/cli@latest generate --config auth.ts
# Apply migrations to create the database tables
pnpm dlx @better-auth/cli@latest migrate --config auth.ts -y
Create Protected Actor
Use the onAuth
hook to validate sessions:
import { actor , setup } from "@rivetkit/actor" ;
import { Unauthorized } from "@rivetkit/actor/errors" ;
import { auth } from "./auth" ;
export const chatRoom = actor ({
// Validate authentication before actor access
onAuth : async ( opts ) => {
const { req } = opts ;
// Use Better Auth to validate the session
const authResult = await auth . api . getSession ({
headers: req . headers ,
});
if (! authResult ) throw new Unauthorized ();
// Return user data to be available in actor
return {
user: authResult . user ,
session: authResult . session ,
};
},
state: {
messages: [] as Array <{
id : string ;
userId : string ;
username : string ;
message : string ;
timestamp : number ;
}>,
},
actions: {
sendMessage : ( c , message : string ) => {
// Access authenticated user data
const { user } = c . conn . auth ;
const newMessage = {
id: crypto . randomUUID (),
userId: user . id ,
username: user . name ,
message ,
timestamp: Date . now (),
};
c . state . messages . push ( newMessage );
c . broadcast ( "newMessage" , newMessage );
return newMessage ;
},
getMessages : ( c ) => c . state . messages ,
},
});
export const registry = setup ({
use: { chatRoom },
});
Setup Server with CORS
Configure your server to handle Better Auth routes and RivetKit:
// server.ts
import { registry } from "./registry" ;
import { auth } from "./auth" ;
import { Hono } from "hono" ;
import { cors } from "hono/cors" ;
import { ALLOWED_PUBLIC_HEADERS } from "@rivetkit/actor" ;
const { serve } = registry . createServer ();
const app = new Hono ();
// Configure CORS for Better Auth + RivetKit
app . use ( "*" , cors ({
// Where your frontend is running
origin: [ "http://localhost:5173" ],
// ALLOWED_PUBLIC_HEADERS are headers required for RivetKit to operate
allowHeaders: [ "Authorization" , ... ALLOWED_PUBLIC_HEADERS ],
allowMethods: [ "POST" , "GET" , "OPTIONS" ],
exposeHeaders: [ "Content-Length" ],
maxAge: 600 ,
credentials: true ,
}));
// Mount Better Auth routes
app . on ([ "GET" , "POST" ], "/api/auth/**" , ( c ) =>
auth . handler ( c . req . raw )
);
// Start RivetKit server
serve ( app );
Frontend Integration
Setup Better Auth Client
Create a Better Auth client for your frontend:
// auth-client.ts
import { createAuthClient } from "better-auth/react" ;
export const authClient = createAuthClient ({
baseURL: "http://localhost:8080" ,
});
Authentication Form
Create login/signup forms:
// AuthForm.tsx
import React , { useState } from "react" ;
import { authClient } from "./auth-client" ;
export function AuthForm () {
const [ isLogin , setIsLogin ] = useState ( true );
const [ email , setEmail ] = useState ( "" );
const [ password , setPassword ] = useState ( "" );
const [ name , setName ] = useState ( "" );
const handleSubmit = async ( e : React . FormEvent ) => {
e . preventDefault ();
try {
if ( isLogin ) {
await authClient . signIn . email ({ email , password });
} else {
await authClient . signUp . email ({ email , password , name });
}
} catch ( error ) {
console . error ( "Auth error:" , error );
}
};
return (
< form onSubmit = { handleSubmit } >
< h2 > { isLogin ? "Sign In" : "Sign Up" } </ h2 >
{ ! isLogin && (
< input
type = "text"
placeholder = "Name"
value = { name }
onChange = { ( e ) => setName ( e . target . value ) }
required
/>
) }
< input
type = "email"
placeholder = "Email"
value = { email }
onChange = { ( e ) => setEmail ( e . target . value ) }
required
/>
< input
type = "password"
placeholder = "Password"
value = { password }
onChange = { ( e ) => setPassword ( e . target . value ) }
required
/>
< button type = "submit" >
{ isLogin ? "Sign In" : "Sign Up" }
</ button >
< button
type = "button"
onClick = { () => setIsLogin (! isLogin ) }
>
{ isLogin ? "Need an account?" : "Have an account?" }
</ button >
</ form >
);
}
Integrate with RivetKit
Use authenticated sessions with RivetKit:
// ChatRoom.tsx
import React , { useState } from "react" ;
import { createClient } from "@rivetkit/client" ;
import { createRivetKit } from "@rivetkit/react" ;
import { authClient } from "./auth-client" ;
import type { registry } from "../backend/registry" ;
const client = createClient < typeof registry >( "http://localhost:8080" );
const { useActor } = createRivetKit ( client );
interface ChatRoomProps {
session : { user : { id : string ; name : string } };
roomId : string ;
}
export function ChatRoom ({ session , roomId }: ChatRoomProps ) {
const [ newMessage , setNewMessage ] = useState ( "" );
const chatRoom = useActor ({
name: "chatRoom" ,
key: [ roomId ],
});
const sendMessage = async () => {
if (! newMessage . trim ()) return ;
await chatRoom . sendMessage ( newMessage );
setNewMessage ( "" );
};
return (
< div >
< div >
< span > Welcome, { session . user . name } ! </ span >
< button onClick = { () => authClient . signOut () } > Sign Out </ button >
</ div >
< div >
{ chatRoom . state . messages . map ( msg => (
< div key = { msg . id } >
< strong > { msg . username } : </ strong > { msg . message }
</ div >
)) }
</ div >
< div >
< input
value = { newMessage }
onChange = { ( e ) => setNewMessage ( e . target . value ) }
onKeyPress = { ( e ) => e . key === "Enter" && sendMessage () }
placeholder = "Type a message..."
/>
< button onClick = { sendMessage } > Send </ button >
</ div >
</ div >
);
}
Advanced Features
Role-Based Access
Add role checking to your actors:
export const adminActor = actor ({
onAuth : async ( opts ) => {
const authResult = await auth . api . getSession ({
headers: opts . req . headers ,
});
if (! authResult ) throw new Unauthorized ();
return { user: authResult . user };
},
actions: {
deleteUser : ( c , userId : string ) => {
// Check user role (assuming you store roles in user data)
const { user } = c . conn . auth ;
if ( user . role !== "admin" ) {
throw new Unauthorized ( "Admin access required" );
}
// Admin-only action
// ... implementation
},
},
});
Session Management
Handle session expiration gracefully:
// hooks/useAuth.ts
import { authClient } from "./auth-client" ;
import { useEffect } from "react" ;
export function useAuthWithRefresh () {
const { data : session , error } = authClient . useSession ();
useEffect (() => {
if ( error ?. message ?. includes ( "session" )) {
// Redirect to login on session expiration
window . location . href = "/login" ;
}
}, [ error ]);
return session ;
}
Production Deployment
For production, you’ll need a database from a provider like Neon , PlanetScale , AWS RDS , or Google Cloud SQL .
Configure your production database connection:
// auth.ts
import { betterAuth } from "better-auth" ;
import { Pool } from "pg" ;
export const auth = betterAuth ({
database: new Pool ({
connectionString: process . env . DATABASE_URL ,
}),
trustedOrigins: [ process . env . FRONTEND_URL ],
emailAndPassword: { enabled: true },
});
Set the following environment variables for production:
DATABASE_URL = postgresql://username:password@localhost:5432/myapp
FRONTEND_URL = https://myapp.com
BETTER_AUTH_SECRET = your-secure-secret-key
BETTER_AUTH_URL = https://api.myapp.com
Read more about configuring Postgres with Better Auth .
Don’t forget to re-generate & re-apply your database migrations if you change the database in your Better Auth config.