UNPKG

@daveyplate/better-auth-instantdb

Version:

Better Auth InstantDB Adapter

406 lines (340 loc) 10.5 kB
# 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