Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions examples/better-auth-external-db/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Dependencies
node_modules
.pnpm-debug.log*

# Build outputs
dist
.turbo

# Environment variables
.env
.env.local
.env.production.local
.env.development.local

# Database
*.sqlite
*.db

# IDE
.vscode
.idea

# OS
.DS_Store
Thumbs.db

# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Better Auth Integration for RivetKit
# Better Auth with External Database for RivetKit

Example project demonstrating authentication integration with [RivetKit](https://rivetkit.org) using Better Auth.
Example project demonstrating authentication integration with [RivetKit](https://rivetkit.org) using Better Auth and SQLite database.

[Learn More →](https://github.com/rivet-gg/rivetkit)

Expand All @@ -17,10 +17,20 @@ Example project demonstrating authentication integration with [RivetKit](https:/

```sh
git clone https://github.com/rivet-gg/rivetkit
cd rivetkit/examples/better-auth
cd rivetkit/examples/better-auth-external-db
npm install
```

### Database Setup

Initialize the SQLite database with Better Auth tables:

```sh
npm run db:setup
```

This will create the `auth.sqlite` database file with the required tables for user authentication.

### Development

```sh
Expand All @@ -34,21 +44,26 @@ Open your browser to `http://localhost:5173` to see the frontend and the backend
- **Authentication**: Email/password authentication using Better Auth
- **Protected Actors**: RivetKit actors with authentication via `onAuth` hook
- **Real-time Chat**: Authenticated chat room with real-time messaging
- **SQLite Database**: Persistent user data and session storage
- **External Database**: Shows how to configure Better Auth with external database (SQLite example)

## How It Works

1. **Better Auth Setup**: Configured with SQLite adapter for user storage
1. **Better Auth Setup**: Configured with SQLite database for persistent user storage
2. **Protected Actor**: The `chatRoom` actor uses the `onAuth` hook to verify user sessions
3. **Frontend Integration**: React components handle authentication flow and chat interface
4. **Session Management**: Better Auth handles session creation, validation, and cleanup

## Database Commands

- `npm run db:setup` - Initialize SQLite database with Better Auth tables

## Key Files

- `src/backend/auth.ts` - Better Auth configuration with SQLite
- `src/backend/auth.ts` - Better Auth configuration with SQLite database
- `src/backend/registry.ts` - RivetKit actor with authentication
- `src/frontend/components/AuthForm.tsx` - Login/signup form
- `src/frontend/components/ChatRoom.tsx` - Authenticated chat interface
- `auth.sqlite` - SQLite database file (auto-created)

## License

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "example-better-auth",
"name": "example-better-auth-external-db",
"version": "0.9.0-rc.1",
"private": true,
"type": "module",
Expand All @@ -9,23 +9,26 @@
"dev:frontend": "vite",
"build": "vite build",
"check-types": "tsc --noEmit",
"test": "vitest run"
"test": "vitest run",
"db:setup": "tsx scripts/setup-db.ts"
},
"devDependencies": {
"@rivetkit/actor": "workspace:*",
"@types/node": "^22.13.9",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"concurrently": "^8.2.2",
"@rivetkit/actor": "workspace:*",
"tsx": "^3.12.7",
"typescript": "^5.5.2",
"vite": "^5.0.0",
"vitest": "^3.1.1"
},
"dependencies": {
"@rivetkit/react": "workspace:*",
"@types/better-sqlite3": "^7.6.13",
"better-auth": "^1.0.1",
"better-sqlite3": "^11.10.0",
"hono": "^4.7.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
Expand Down
66 changes: 66 additions & 0 deletions examples/better-auth-external-db/scripts/setup-db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Database from "better-sqlite3";

// Create SQLite database and tables for Better Auth
const db = new Database("/tmp/auth.sqlite");

// Create user table
db.exec(`
CREATE TABLE IF NOT EXISTS user (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
emailVerified BOOLEAN NOT NULL DEFAULT FALSE,
name TEXT NOT NULL,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL
);
`);

// Create session table
db.exec(`
CREATE TABLE IF NOT EXISTS session (
id TEXT PRIMARY KEY,
userId TEXT NOT NULL,
expiresAt INTEGER NOT NULL,
token TEXT UNIQUE NOT NULL,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL,
ipAddress TEXT,
userAgent TEXT,
FOREIGN KEY (userId) REFERENCES user(id) ON DELETE CASCADE
);
`);

// Create account table
db.exec(`
CREATE TABLE IF NOT EXISTS account (
id TEXT PRIMARY KEY,
userId TEXT NOT NULL,
accountId TEXT NOT NULL,
providerId TEXT NOT NULL,
accessToken TEXT,
refreshToken TEXT,
idToken TEXT,
accessTokenExpiresAt INTEGER,
refreshTokenExpiresAt INTEGER,
scope TEXT,
password TEXT,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL,
FOREIGN KEY (userId) REFERENCES user(id) ON DELETE CASCADE
);
`);

// Create verification table
db.exec(`
CREATE TABLE IF NOT EXISTS verification (
id TEXT PRIMARY KEY,
identifier TEXT NOT NULL,
value TEXT NOT NULL,
expiresAt INTEGER NOT NULL,
createdAt INTEGER NOT NULL,
updatedAt INTEGER NOT NULL
);
`);

console.log("Database tables created successfully!");
db.close();
10 changes: 10 additions & 0 deletions examples/better-auth-external-db/src/backend/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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,
},
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { actor, OnAuthOptions, setup, UserError } from "@rivetkit/actor";
import { actor, setup, type OnAuthOptions } from "@rivetkit/actor";
import { Unauthorized } from "@rivetkit/actor/errors";
import { auth } from "./auth";

interface State {
Expand All @@ -13,17 +14,25 @@ interface Message {
timestamp: number;
}










export const chatRoom = actor({
// onAuth runs on the server & before connecting to the actor
onAuth: async (c: OnAuthOptions) => {
// ✨ NEW ✨ Access Better Auth session
const authResult = await auth.api.getSession({
headers: c.req.headers,
});
console.log("auth result", authResult);

if (!authResult?.session || !authResult?.user) {
throw new UserError("Unauthorized");
}
if (!authResult) throw new Unauthorized();

// Passes auth data to the actor (c.conn.auth)
return {
user: authResult.user,
session: authResult.session,
Expand All @@ -34,10 +43,11 @@ export const chatRoom = actor({
} as State,
actions: {
sendMessage: (c, message: string) => {
// ✨ NEW ✨ — Access Better Auth with c.conn.auth
const newMessage = {
id: crypto.randomUUID(),
userId: "TODO",
username: c.conn.auth.user.email || "Unknown",
userId: c.conn.auth.user.id,
username: c.conn.auth.user.name,
message,
timestamp: Date.now(),
};
Expand All @@ -53,6 +63,17 @@ export const chatRoom = actor({
},
});












export const registry = setup({
use: { chatRoom },
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { registry } from "./registry";
import { auth } from "./auth";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { ALLOWED_PUBLIC_HEADERS } from "@rivetkit/actor";

// Start RivetKit
const { client, hono, serve } = registry.createServer();
const { serve } = registry.createServer();

// Setup router
const app = new Hono();
Expand All @@ -13,7 +14,8 @@ app.use(
"*",
cors({
origin: ["http://localhost:5173"],
allowHeaders: ["Content-Type", "Authorization"],
// Need to allow custom headers used in RivetKit
allowHeaders: ["Authorization", ...ALLOWED_PUBLIC_HEADERS],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createClient, createRivetKit } from "@rivetkit/react";
import { authClient } from "../auth-client";
import type { registry } from "../../backend/registry";

const client = createClient<typeof registry>("http://localhost:8080/registry");
const client = createClient<typeof registry>("http://localhost:8080");

const { useActor } = createRivetKit(client);

Expand Down
17 changes: 0 additions & 17 deletions examples/better-auth/src/backend/auth.ts

This file was deleted.

2 changes: 1 addition & 1 deletion examples/hono-react/src/frontend/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useState } from "react";
import { createClient, createRivetKit } from "@rivetkit/react";
import type { registry } from "../backend/registry";

const client = createClient<typeof registry>("http://localhost:8080/registry");
const client = createClient<typeof registry>("http://localhost:8080");
const { useActor } = createRivetKit<typeof registry>(client);

function App() {
Expand Down
2 changes: 1 addition & 1 deletion examples/react/src/frontend/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useState } from "react";
import { createClient, createRivetKit } from "@rivetkit/react";
import type { registry } from "../backend/registry";

const client = createClient<typeof registry>(`http://localhost:8080/registry`);
const client = createClient<typeof registry>(`http://localhost:8080`);
const { useActor } = createRivetKit(client);

function App() {
Expand Down
16 changes: 14 additions & 2 deletions packages/core/src/actor/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ export class InvalidStateType extends ActorError {
} else {
msg += "Attempted to set invalid state.";
}
msg += " State must be CBOR serializable. Valid types include: null, undefined, boolean, string, number, BigInt, Date, RegExp, Error, typed arrays (Uint8Array, Int8Array, Float32Array, etc.), Map, Set, Array, and plain objects.";
msg +=
" State must be CBOR serializable. Valid types include: null, undefined, boolean, string, number, BigInt, Date, RegExp, Error, typed arrays (Uint8Array, Int8Array, Float32Array, etc.), Map, Set, Array, and plain objects.";
super("invalid_state_type", msg);
}
}
Expand Down Expand Up @@ -289,9 +290,20 @@ export class InvalidParams extends ActorError {
}
}

export class Unauthorized extends ActorError {
constructor(message?: string) {
super("unauthorized", message ?? "Unauthorized. Access denied.", {
public: true,
});
this.statusCode = 401;
}
}

export class Forbidden extends ActorError {
constructor(message?: string) {
super("forbidden", message ?? "Access denied", { public: true });
super("forbidden", message ?? "Forbidden. Access denied.", {
public: true,
});
this.statusCode = 403;
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/actor/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ export type {
ActorContextOf,
ActionContextOf,
} from "./definition";
export { ALLOWED_PUBLIC_HEADERS} from "./router-endpoints";
export { ALLOWED_PUBLIC_HEADERS } from "./router-endpoints";
Loading
Loading