@daveyplate/better-auth-instantdb v1.1.1
Better Auth InstantDB Adapter
A seamless integration between Better Auth and InstantDB that allows you to use InstantDB as your authentication database.
- Own Your Auth
š @daveycodez
āļø Buy me a coffee
Installation
pnpm add @daveyplate/better-auth-instantdb@latest
Features
- š Complete Authentication: Leverage Better Auth's authentication features with InstantDB as your database
- š Session Sync: Automatically synchronize auth sessions between Better Auth and InstantDB
- š ļø Customizable: Configure the adapter to match your specific needs
- š§© Type-Safe: Fully typed with TypeScript for improved developer experience
Usage
Basic Setup
First you need to add the InstantDB Adapter to your Better Auth config.
auth.ts
import { betterAuth } from 'better-auth'
import { instantDBAdapter } from '@daveyplate/better-auth-instantdb'
import { init } from "@instantdb/admin";
import schema from "instant.schema";
// Create InstantDB admin client
const adminDb = init({
appId: process.env.INSTANT_APP_ID,
apiKey: process.env.INSTANT_API_KEY
schema
});
// Create Better Auth instance with InstantDB adapter
export const auth = betterAuth({
database: instantDBAdapter({
db: adminDb,
usePlural: true, // Optional: set to true if your schema uses plural table names
debugLogs: false // Optional: set to true to see detailed logs
}),
// Other Better Auth configuration options
emailAndPassword: { enabled: true }
})
Client-Side Usage
Synchronize authentication state between Better Auth and InstantDB:
providers.tsx
"use client"
import { useSession } from '@/lib/auth-client'
import { init } from '@instantdb/react'
import { useInstantAuth } from '@daveyplate/better-auth-instantdb'
// Initialize InstantDB client
const db = init({
appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID
})
export function Providers({ children }) {
const { data: sessionData, isPending } = useSession()
// Set up InstantDB auth sync with Better Auth
useInstantAuth({
db,
sessionData,
isPending
})
return (
// Your application code
{children}
)
}
InstantDB Schema and Permissions Setup
ā ļø Important: You must manually create and configure the InstantDB schema and permissions files for this adapter to work correctly.
1. Create Schema File
Create an instant.schema.ts
file with the required entities for Better Auth:
instant.schema.ts
import { i } from "@instantdb/react";
const _schema = i.schema({
entities: {
// System entities
$files: i.entity({
path: i.string().unique().indexed(),
url: i.any(),
}),
$users: i.entity({
email: i.string().unique().indexed(),
}),
// Authentication entities
users: i.entity({
createdAt: i.date(),
email: i.string().unique(),
emailVerified: i.boolean(),
image: i.string(),
name: i.string(),
updatedAt: i.date(),
}),
sessions: i.entity({
createdAt: i.date(),
expiresAt: i.date().indexed(),
ipAddress: i.string(),
token: i.string(),
updatedAt: i.date(),
userAgent: i.string(),
userId: i.string(),
}),
accounts: i.entity({
accessToken: i.string(),
accessTokenExpiresAt: i.date(),
accountId: i.string(),
createdAt: i.date(),
idToken: i.string(),
password: i.string(),
providerId: i.string(),
refreshToken: i.string(),
refreshTokenExpiresAt: i.date(),
scope: i.string(),
updatedAt: i.date(),
userId: i.string().indexed(),
}),
verifications: i.entity({
createdAt: i.date().indexed(),
expiresAt: i.date().indexed(),
identifier: i.string(),
updatedAt: i.date(),
value: i.string(),
}),
// Optional entities for additional features (public profile example)
profiles: i.entity({
createdAt: i.date(),
image: i.string(),
name: i.string(),
updatedAt: i.date(),
}),
},
links: {
// Required links for auth
users$user: {
forward: {
on: "users",
has: "one",
label: "$user",
onDelete: "cascade",
},
reverse: {
on: "$users",
has: "one",
label: "user",
},
},
sessionsUser: {
forward: {
on: "sessions",
has: "one",
label: "user",
onDelete: "cascade",
},
reverse: {
on: "users",
has: "many",
label: "sessions",
},
},
accountsUser: {
forward: {
on: "accounts",
has: "one",
label: "user",
onDelete: "cascade",
},
reverse: {
on: "users",
has: "many",
label: "accounts",
},
},
// Optional links (public profile example)
profilesUser: {
forward: {
on: "profiles",
has: "one",
label: "user",
onDelete: "cascade",
},
reverse: {
on: "users",
has: "one",
label: "profile",
},
},
// Add your custom links here
},
});
// This helps TypeScript display nicer intellisense
type _AppSchema = typeof _schema;
interface AppSchema extends _AppSchema {}
const schema: AppSchema = _schema;
export type { AppSchema };
export default schema;
2. Create Permissions File
Create an instant.perms.ts
file to secure your schema:
// instant.perms.ts
import type { InstantRules } from "@instantdb/react";
const rules = {
// Prevent creation of new attributes without explicit schema changes
attrs: {
allow: {
$default: "false",
},
},
// Auth entities permissions
users: {
bind: ["isOwner", "auth.id != null && auth.id == data.id"],
allow: {
view: "isOwner",
create: "false",
delete: "false",
update: "isOwner && (newData.email == data.email) && (newData.emailVerified == data.emailVerified) && (newData.createdAt == data.createdAt)",
},
},
accounts: {
bind: ["isOwner", "auth.id != null && auth.id == data.userId"],
allow: {
view: "isOwner",
create: "false",
delete: "false",
update: "false",
},
},
sessions: {
bind: ["isOwner", "auth.id != null && auth.id == data.userId"],
allow: {
view: "isOwner",
create: "false",
delete: "false",
update: "false",
},
},
verifications: {
allow: {
$default: "false"
}
},
// Optional permissions (public profile example)
profiles: {
bind: ["isOwner", "auth.id != null && auth.id == data.id"],
allow: {
view: "true",
create: "false",
delete: "false",
update: "isOwner",
},
},
// Add your custom entity permissions here
} satisfies InstantRules;
export default rules;
3. Push Schema and Permissions to InstantDB
After creating these files, use the InstantDB CLI to push them to your app:
# Push schema
npx instant-cli@latest push schema
# Push permissions
npx instant-cli@latest push perms
4. Initialize InstantDB with Your Schema
Update your client-side InstantDB initialization to use your schema:
/database/instant.ts
import { init } from "@instantdb/react"
import schema from "../../instant.schema"
export const db = init({
appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID,
schema
})
API Reference
instantDBAdapter(options)
Creates an adapter that allows Better Auth to use InstantDB as its database.
Options
Option | Type | Default | Description |
---|---|---|---|
db | InstantAdminDatabase | (required) | An InstantDB admin client instance |
usePlural | boolean | true | Set to false if your schema uses singular table names |
debugLogs | boolean | false | Set to true to enable detailed logging |
transactionHooks | Promise<TransactionChunk<any, any>[]> | undefined | Custom hooks for create and update operations |
useInstantAuth({ db, useSession })
A React hook that synchronizes authentication state between Better Auth and InstantDB.
Parameters
Parameter | Type | Description |
---|---|---|
db | InstantReactWebDatabase | An InstantDB client instance |
useSession | function | The useSession hook from Better Auth |
useInstantAuth({ db, sessionData, isPending })
An alternative form of the React hook that synchronizes authentication state between Better Auth and InstantDB.
Parameters
Parameter | Type | Description |
---|---|---|
db | InstantReactWebDatabase | An InstantDB client instance |
sessionData | { session: Session; user: User } \| null | Session data from Better Auth |
isPending | boolean | Whether the session data is still loading |
Advanced Usage
Custom Transaction Hooks
You can extend the adapter's behavior with custom transaction hooks:
Sync public profile with user entity
instantDBAdapter({
db,
usePlural: true,
transactionHooks: {
create: async ({ model, data }) => {
if (model === "users") {
const transactions = [
db.tx.profiles[data.id]
.update({
name: data.name,
image: data.image,
createdAt: Date.now(),
updatedAt: Date.now()
})
.link({ user: data.id })
]
return transactions
}
},
update: async ({ model, update, where }) => {
if (model === "users") {
const result = await db.query({ profiles: { $: { where: parseWhere(where) } } })
return result.profiles.map((profile) =>
db.tx.profiles[profile.id].update({
name: update.name,
image: update.image,
updatedAt: Date.now()
})
)
}
}
}
})
License
MIT