Auth
Better Auth
Integrate Rivet with Better Auth for authentication
Better Auth provides a comprehensive authentication solution that integrates seamlessly with Rivet Actors using the onAuth
hook.
Check out the complete example
Install Better Auth alongside Rivet:
npm install better-auth better-sqlite3
npm install -D @types/better-sqlite3
# For React integration
npm install @rivetkit/react
NoteThis example uses SQLite to keep the example. In production, replace this with a database like Postgres. Read more about configuring your database in Better Auth .
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 Rivet:
// 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 + Rivet
app .use ( "*" , cors ({
// Where your frontend is running
origin : [ "http://localhost:5173" ] ,
// ALLOWED_PUBLIC_HEADERS are headers required for Rivet 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 Rivet server
serve (app);
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 Rivet Use authenticated sessions with Rivet:
// 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 >
);
}
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
} ,
} ,
});
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;
}
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 .
TipDon't forget to re-generate & re-apply your database migrations if you change the database in your Better Auth config.