diff --git a/examples/better-auth/src/backend/auth.ts b/examples/better-auth/src/backend/auth.ts
index c204f1e62..1599dc4cf 100644
--- a/examples/better-auth/src/backend/auth.ts
+++ b/examples/better-auth/src/backend/auth.ts
@@ -1,21 +1,17 @@
import { betterAuth } from "better-auth";
-import { sqliteAdapter } from "@better-auth/sqlite";
-import Database from "better-sqlite3";
-
-const db = new Database("./auth.db");
export const auth = betterAuth({
- // IMPORTANT: Connect your own database here
- database: sqliteAdapter(db),
+ // IMPORTANT: Connect a real database for productoin use cases
+ //
+ // https://www.better-auth.com/docs/installation#create-database-tables
+ // database: memoryAdapter({
+ // user: [],
+ // account: [],
+ // session: [],
+ // verifcation: [],
+ // }),
+ trustedOrigins: ["http://localhost:5173"],
emailAndPassword: {
enabled: true,
},
- session: {
- expiresIn: 60 * 60 * 24 * 7, // 7 days
- updateAge: 60 * 60 * 24, // 1 day (every day the session expiry is updated)
- },
- plugins: [],
});
-
-export type Session = typeof auth.$Infer.Session;
-export type User = typeof auth.$Infer.User;
diff --git a/examples/better-auth/src/backend/registry.ts b/examples/better-auth/src/backend/registry.ts
index 8d2a17003..d0b117964 100644
--- a/examples/better-auth/src/backend/registry.ts
+++ b/examples/better-auth/src/backend/registry.ts
@@ -1,31 +1,43 @@
-import { actor, setup } from "@rivetkit/actor";
-import { auth, type Session, type User } from "./auth";
+import { actor, OnAuthOptions, setup, UserError } from "@rivetkit/actor";
+import { auth } from "./auth";
+
+interface State {
+ messages: Message[];
+}
+
+interface Message {
+ id: string;
+ userId: string;
+ username: string;
+ message: string;
+ timestamp: number;
+}
export const chatRoom = actor({
- onAuth: async (c) => {
+ onAuth: async (c: OnAuthOptions) => {
const authResult = await auth.api.getSession({
headers: c.req.headers,
});
+ console.log("auth result", authResult);
if (!authResult?.session || !authResult?.user) {
- throw new Error("Unauthorized");
+ throw new UserError("Unauthorized");
}
return {
- userId: authResult.user.id,
user: authResult.user,
session: authResult.session,
};
},
- state: {
- messages: [] as Array<{ id: string; userId: string; username: string; message: string; timestamp: number }>
- },
+ state: {
+ messages: [],
+ } as State,
actions: {
sendMessage: (c, message: string) => {
const newMessage = {
id: crypto.randomUUID(),
- userId: c.auth.userId,
- username: c.auth.user.email,
+ userId: "TODO",
+ username: c.conn.auth.user.email || "Unknown",
message,
timestamp: Date.now(),
};
@@ -44,4 +56,3 @@ export const chatRoom = actor({
export const registry = setup({
use: { chatRoom },
});
-
diff --git a/examples/better-auth/src/backend/server.ts b/examples/better-auth/src/backend/server.ts
index dc9b9420a..25ee05782 100644
--- a/examples/better-auth/src/backend/server.ts
+++ b/examples/better-auth/src/backend/server.ts
@@ -1,54 +1,27 @@
import { registry } from "./registry";
import { auth } from "./auth";
import { Hono } from "hono";
-import { serve } from "@hono/node-server";
+import { cors } from "hono/cors";
+
+// Start RivetKit
+const { client, hono, serve } = registry.createServer();
// Setup router
const app = new Hono();
-// Start RivetKit
-const { client, hono } = registry.run({
- driver: createMemoryDriver(),
- cors: {
- // IMPORTANT: Configure origins in production
- origin: "*",
- },
-});
+app.use(
+ "*",
+ cors({
+ origin: ["http://localhost:5173"],
+ allowHeaders: ["Content-Type", "Authorization"],
+ 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));
-// Expose RivetKit to the frontend
-app.route("/registry", hono);
-
-// Example HTTP endpoint to join chat room
-app.post("/api/join-room/:roomId", async (c) => {
- const roomId = c.req.param("roomId");
-
- // Verify authentication
- const authResult = await auth.api.getSession({
- headers: c.req.header(),
- });
-
- if (!authResult?.session || !authResult?.user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- try {
- const room = client.chatRoom.getOrCreate(roomId);
- const messages = await room.getMessages();
-
- return c.json({
- success: true,
- roomId,
- messages,
- user: authResult.user
- });
- } catch (error) {
- return c.json({ error: "Failed to join room" }, 500);
- }
-});
-
-serve({ fetch: app.fetch, port: 8080 }, () =>
- console.log("Listening at http://localhost:8080"),
-);
+serve(app);
diff --git a/examples/better-auth/src/frontend/components/AuthForm.tsx b/examples/better-auth/src/frontend/components/AuthForm.tsx
index 005e89696..352c876a0 100644
--- a/examples/better-auth/src/frontend/components/AuthForm.tsx
+++ b/examples/better-auth/src/frontend/components/AuthForm.tsx
@@ -8,6 +8,7 @@ interface AuthFormProps {
export function AuthForm({ onAuthSuccess }: AuthFormProps) {
const [isLogin, setIsLogin] = useState(true);
const [email, setEmail] = useState("");
+ const [name, setName] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
@@ -26,6 +27,7 @@ export function AuthForm({ onAuthSuccess }: AuthFormProps) {
} else {
await authClient.signUp.email({
email,
+ name,
password,
});
}
@@ -54,6 +56,20 @@ export function AuthForm({ onAuthSuccess }: AuthFormProps) {
/>
+ {!isLogin && (
+
+
+ setName(e.target.value)}
+ required
+ style={{ width: "100%", padding: "8px", marginTop: "5px" }}
+ />
+
+ )}
+
("http://localhost:8080/registry");
+const client = createClient("http://localhost:8080/registry");
const { useActor } = createRivetKit(client);
@@ -30,7 +30,13 @@ export function ChatRoom({ user, onSignOut }: ChatRoomProps) {
// Listen for new messages
chatRoom.useEvent("newMessage", (newMessage) => {
- setMessages(prev => [...prev, newMessage]);
+ setMessages(prev => [...prev, newMessage as {
+ id: string;
+ userId: string;
+ username: string;
+ message: string;
+ timestamp: number;
+ }]);
});
// Load initial messages when connected
diff --git a/packages/core/src/actor/errors.ts b/packages/core/src/actor/errors.ts
index d474334a3..7503ab1f7 100644
--- a/packages/core/src/actor/errors.ts
+++ b/packages/core/src/actor/errors.ts
@@ -182,7 +182,7 @@ export class InvalidStateType extends ActorError {
} else {
msg += "Attempted to set invalid state.";
}
- msg += " State must be JSON serializable.";
+ 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);
}
}
diff --git a/packages/core/src/actor/instance.ts b/packages/core/src/actor/instance.ts
index 5cf1ec9ba..640b59dbb 100644
--- a/packages/core/src/actor/instance.ts
+++ b/packages/core/src/actor/instance.ts
@@ -3,7 +3,7 @@ import type * as wsToClient from "@/actor/protocol/message/to-client";
import type * as wsToServer from "@/actor/protocol/message/to-server";
import type { Client } from "@/client/client";
import type { Logger } from "@/common/log";
-import { isJsonSerializable, stringifyError } from "@/common/utils";
+import { isCborSerializable, stringifyError } from "@/common/utils";
import type { Registry } from "@/mod";
import invariant from "invariant";
import onChange from "on-change";
@@ -454,7 +454,7 @@ export class ActorInstance {
if (target === null || typeof target !== "object") {
let invalidPath = "";
if (
- !isJsonSerializable(
+ !isCborSerializable(
target,
(path) => {
invalidPath = path;
@@ -479,7 +479,7 @@ export class ActorInstance {
(path: string, value: any, _previousValue: any, _applyData: any) => {
let invalidPath = "";
if (
- !isJsonSerializable(
+ !isCborSerializable(
value,
(invalidPathPart) => {
invalidPath = invalidPathPart;
diff --git a/packages/core/src/common/utils.ts b/packages/core/src/common/utils.ts
index 1c6e61b6a..d56bf1a8d 100644
--- a/packages/core/src/common/utils.ts
+++ b/packages/core/src/common/utils.ts
@@ -37,10 +37,13 @@ export function safeStringify(obj: unknown, maxSize: number) {
// it. Roll back state if fails to serialize.
/**
- * Check if a value is JSON serializable.
+ * Check if a value is CBOR serializable.
* Optionally pass an onInvalid callback to receive the path to invalid values.
+ *
+ * For a complete list of supported CBOR tags, see:
+ * https://github.com/kriszyp/cbor-x/blob/cc1cf9df8ba72288c7842af1dd374d73e34cdbc1/README.md#list-of-supported-tags-for-decoding
*/
-export function isJsonSerializable(
+export function isCborSerializable(
value: unknown,
onInvalid?: (path: string) => void,
currentPath = "",
@@ -62,30 +65,98 @@ export function isJsonSerializable(
return true;
}
+ // Handle BigInt (CBOR tags 2 and 3)
+ if (typeof value === "bigint") {
+ return true;
+ }
+
+ // Handle Date objects (CBOR tags 0 and 1)
+ if (value instanceof Date) {
+ return true;
+ }
+
+ // Handle typed arrays (CBOR tags 64-82)
+ if (
+ value instanceof Uint8Array ||
+ value instanceof Uint8ClampedArray ||
+ value instanceof Uint16Array ||
+ value instanceof Uint32Array ||
+ value instanceof BigUint64Array ||
+ value instanceof Int8Array ||
+ value instanceof Int16Array ||
+ value instanceof Int32Array ||
+ value instanceof BigInt64Array ||
+ value instanceof Float32Array ||
+ value instanceof Float64Array
+ ) {
+ return true;
+ }
+
+ // Handle Map (CBOR tag 259)
+ if (value instanceof Map) {
+ for (const [key, val] of value.entries()) {
+ const keyPath = currentPath ? `${currentPath}.key(${String(key)})` : `key(${String(key)})`;
+ const valPath = currentPath ? `${currentPath}.value(${String(key)})` : `value(${String(key)})`;
+ if (!isCborSerializable(key, onInvalid, keyPath) || !isCborSerializable(val, onInvalid, valPath)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Handle Set (CBOR tag 258)
+ if (value instanceof Set) {
+ let index = 0;
+ for (const item of value.values()) {
+ const itemPath = currentPath ? `${currentPath}.set[${index}]` : `set[${index}]`;
+ if (!isCborSerializable(item, onInvalid, itemPath)) {
+ return false;
+ }
+ index++;
+ }
+ return true;
+ }
+
+ // Handle RegExp (CBOR tag 27)
+ if (value instanceof RegExp) {
+ return true;
+ }
+
+ // Handle Error objects (CBOR tag 27)
+ if (value instanceof Error) {
+ return true;
+ }
+
// Handle arrays
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
const itemPath = currentPath ? `${currentPath}[${i}]` : `[${i}]`;
- if (!isJsonSerializable(value[i], onInvalid, itemPath)) {
+ if (!isCborSerializable(value[i], onInvalid, itemPath)) {
return false;
}
}
return true;
}
- // Handle plain objects
+ // Handle plain objects and records (CBOR tags 105, 51, 57344-57599)
if (typeof value === "object") {
- // Reject if it's not a plain object
- if (Object.getPrototypeOf(value) !== Object.prototype) {
- onInvalid?.(currentPath);
- return false;
+ // Allow plain objects and objects with prototypes (for records and named objects)
+ const proto = Object.getPrototypeOf(value);
+ if (proto !== null && proto !== Object.prototype) {
+ // Check if it's a known serializable object type
+ const constructor = value.constructor;
+ if (constructor && typeof constructor.name === "string") {
+ // Allow objects with named constructors (records, named objects)
+ // This includes user-defined classes and built-in objects
+ // that CBOR can serialize with tag 27 or record tags
+ }
}
// Check all properties recursively
for (const key in value) {
const propPath = currentPath ? `${currentPath}.${key}` : key;
if (
- !isJsonSerializable(
+ !isCborSerializable(
value[key as keyof typeof value],
onInvalid,
propPath,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 919477b5e..36b1918d6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -57,6 +57,9 @@ importers:
better-auth:
specifier: ^1.0.1
version: 1.2.10
+ better-sqlite3:
+ specifier: ^9.4.3
+ version: 9.6.0
hono:
specifier: ^4.7.0
version: 4.8.0
@@ -70,6 +73,9 @@ importers:
'@rivetkit/actor':
specifier: workspace:*
version: link:../../packages/actor
+ '@types/better-sqlite3':
+ specifier: ^7.6.9
+ version: 7.6.13
'@types/node':
specifier: ^22.13.9
version: 22.15.32
@@ -2231,6 +2237,9 @@ packages:
better-sqlite3@11.10.0:
resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==}
+ better-sqlite3@9.6.0:
+ resolution: {integrity: sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ==}
+
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
@@ -5597,6 +5606,11 @@ snapshots:
bindings: 1.5.0
prebuild-install: 7.1.3
+ better-sqlite3@9.6.0:
+ dependencies:
+ bindings: 1.5.0
+ prebuild-install: 7.1.3
+
bindings@1.5.0:
dependencies:
file-uri-to-path: 1.0.0