@daveyplate/better-auth-instantdb
Version:
Better Auth InstantDB Adapter
406 lines (340 loc) • 10.5 kB
Markdown
# Better Auth InstantDB Adapter
A seamless integration between [Better Auth](https://better-auth.com) and [InstantDB](https://www.instantdb.com) that allows you to use InstantDB as your authentication database.
[Better Auth UI Integration](https://better-auth-ui.com/data/instantdb)
- *Own Your Auth*
𝕏 [@daveycodez](https://x.com/daveycodez)
☕️ [Buy me a coffee](https://buymeacoffee.com/daveycodez)
## Installation
```bash
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
```typescript
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
```typescript
"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
```typescript
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:
```typescript
// 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:
```bash
# 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
```typescript
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
```typescript
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