# Common mistakes Common mistakes when working with Instant Below are some common mistakes when working with Instant. ## Common mistakes with schema ❌ **Common mistake**: Reusing the same label for different links ``` // ❌ Bad: Conflicting labels const _schema = i.schema({ links: { postAuthor: { forward: { on: 'posts', has: 'one', label: 'author' }, reverse: { on: 'profiles', has: 'many', label: 'posts' }, // Creates 'posts' attr }, postEditor: { forward: { on: 'posts', has: 'one', label: 'editor' }, reverse: { on: 'profiles', has: 'many', label: 'posts' }, // Conflicts! }, }, }); ``` ✅ **Correction**: Use unique labels for each relationship ``` // ✅ Good: Unique labels for each relationship const _schema = i.schema({ links: { postAuthor: { forward: { on: 'posts', has: 'one', label: 'author' }, reverse: { on: 'profiles', has: 'many', label: 'authoredPosts' }, // Unique }, postEditor: { forward: { on: 'posts', has: 'one', label: 'editor' }, reverse: { on: 'profiles', has: 'many', label: 'editedPosts' }, // Unique }, }, }); ``` ## Common mistakes with permissions Sometimes you want to express permissions based on an attribute in a linked entity. For those instances you can use `data.ref`. ❌ **Common mistake**: Not using `data.ref` to reference linked data ``` // ❌ Bad: This will throw an error! { "comments": { "allow": { "update": "auth.id in data.post.author.id" } } } ``` ``` // ✅ Good: Permission based on linked data { "comments": { "allow": { "update": "auth.id in data.ref('post.author.id')" // Allow post authors to update comments } } } ``` When using `data.ref` the last part of the string is the attribute you want to access. If you do not specify an attribute an error will occur. ❌ **Common mistake**: Not specifying an attribute when using data.ref ``` // ❌ Bad: No attribute specified. This will throw an error! "view": "auth.id in data.ref('author')" ``` ✅ **Correction**: Specify the attribute you want to access ``` // ✅ Good: Correctly using data.ref to reference a linked attribute "view": "auth.id in data.ref('author.id')" ``` `data.ref` will _ALWAYS_ return a CEL list of linked entities. So we must use the `in` operator to check if a value exists in that list. ❌ **Common mistake**: Using `==` to check if a value exists in a list ``` // ❌ Bad: data.ref returns a list! This will throw an error! "view": "data.ref('admins.id') == auth.id" ``` ✅ **Correction**: Use `in` to check if a value exists in a list ``` ✅ Good: Checking if a user is in a list of admins "view": "auth.id in data.ref('admins.id')" ``` Even if you are referencing a one-to-one relationship, `data.ref` will still return a CEL list. You must extract the first element from the list to compare it properly. ❌ **Common mistake**: Using `==` to check if a value matches in a one-to-one relationship ``` // ❌ Bad: data.ref always returns a CEL list. This will throw an error! "view": "auth.id == data.ref('owner.id')" ``` ✅ **Correction**: Use `in` to check a value even for one-to-one relationships ``` // ✅ Good: Extracting the first element from a one-to-one relationship "view": "auth.id in data.ref('owner.id')" ``` Be careful when checking whether there are no linked entities. Here are a few correct ways to do this: ❌ **Common mistake**: Incorrectly checking for an empty list ``` // ❌ Bad: `data.ref` returns a CEL list so checking against null will throw an error! "view": "data.ref('owner.id') != null" // ❌ Bad: `data.ref` is a CEL list and does not support `length` "view": "data.ref('owner.id').length > 0" // ❌ Bad: You must specify an attribute when using `data.ref` "view": "data.ref('owner') != []" ``` ✅ **Correction**: Best way to check for an empty list ``` // ✅ Good: Checking if the list is empty "view": "data.ref('owner.id') != []" ``` Use `auth.ref` to reference the authenticated user's linked data. This behaves similar to `data.ref` but you _MUST_ use the `$user` prefix when referencing auth data: ❌ **Common mistake**: Missing `$user` prefix with `auth.ref` ``` // ❌ Bad: This will throw an error! { "adminActions": { "allow": { "create": "'admin' in auth.ref('role.type')" } } } ``` ✅ **Correction**: Use `$user` prefix with `auth.ref` ``` // ✅ Good: Checking user roles { "adminActions": { "allow": { "create": "'admin' in auth.ref('$user.role.type')" // Allow admins only } } } ``` `auth.ref` returns a CEL list, so use `[0]` to extract the first element when needed. ❌ **Common mistake**: Using `==` to check if auth.ref matches a value ``` // ❌ Bad: auth.ref returns a list! This will throw an error! "create": "auth.ref('$user.role.type') == 'admin'" ``` ✅ **Correction**: Extract the first element from `auth.ref` ``` // ✅ Good: Extracting the first element from auth.ref "create": "auth.ref('$user.role.type')[0] == 'admin'" ``` For update operations, you can compare the existing (`data`) and updated (`newData`) values. One difference between `data.ref` and `newData.ref` is that `newData.ref` does not exist. You can only use `newData` to reference the updated attributes directly. ❌ **Common mistake**: `newData.ref` does not exist. ``` // ❌ Bad: This will throw an error! // This will throw an error because newData.ref does not exist { "posts": { "allow": { "update": "auth.id == data.authorId && newData.ref('isPublished') == data.ref('isPublished')" } } } ``` ❌ **Common mistake**: ref arguments must be string literals ``` // ❌ Bad: This will throw an error! "view": "auth.id in data.ref(someVariable + '.members.id')" ``` ✅ **Correction**: Only string literals are allowed ``` // ✅ Good: Using string literals for ref arguments "view": "auth.id in data.ref('team.members.id')" ``` ## Common mistakes with transactions Use `merge` for updating nested objects without overwriting unspecified fields: ❌ **Common mistake**: Using `update` for nested objects ```typescript // ❌ Bad: This will overwrite the entire preferences object db.transact( db.tx.profiles[userId].update({ preferences: { theme: 'dark' }, // Any other preferences will be lost }), ); ``` ✅ **Correction**: Use `merge` to update nested objects ``` // ✅ Good: Update nested values without losing other data db.transact(db.tx.profiles[userId].merge({ preferences: { theme: "dark" } })); ``` You can use `merge` to remove keys from nested objects by setting the key to `null`: ❌ **Common mistake**: Calling `update` instead of `merge` for removing keys ``` // ❌ Bad: Calling `update` will overwrite the entire preferences object db.transact(db.tx.profiles[userId].update({ preferences: { notifications: null } })); ``` ✅ **Correction**: Use `merge` to remove keys from nested objects ``` // ✅ Good: Remove a nested key db.transact(db.tx.profiles[userId].merge({ preferences: { notifications: null // This will remove the notifications key } })); ``` Large transactions can lead to timeouts. To avoid this, break them into smaller batches: ❌ **Common mistake**: Not batching large transactions leads to timeouts ```typescript import { id } from '@instantdb/react'; const txs = []; for (let i = 0; i < 1000; i++) { txs.push( db.tx.todos[id()].update({ text: `Todo ${i}`, done: false, }), ); } // ❌ Bad: This will likely lead to a timeout! await db.transact(txs); ``` ❌ **Common mistake**: Creating too many transactions will also lead to timeouts ```typescript import { id } from '@instantdb/react'; // ❌ Bad: This will fire 1000 transactions at once and will lead to multiple // timeouts! for (let i = 0; i < 1000; i++) { db.transact( db.tx.todos[id()].update({ text: `Todo ${i}`, done: false, }), ); } await db.transact(txs); ``` ✅ **Correction**: Batch large transactions into smaller ones ``` // ✅ Good: Batch large operations import { id } from '@instantdb/react'; const batchSize = 100; const createManyTodos = async (count) => { for (let i = 0; i < count; i += batchSize) { const batch = []; // Create up to batchSize transactions for (let j = 0; j < batchSize && i + j < count; j++) { batch.push( db.tx.todos[id()].update({ text: `Todo ${i + j}`, done: false }) ); } // Execute this batch await db.transact(batch); } }; // Create 1000 todos in batches createManyTodos(1000); ``` ## Common mistakes with queries Nest namespaces to fetch associated entities: ❌ **Common mistake**: Not nesting namespaces will fetch unrelated entities ``` // ❌ Bad: This will fetch all todos and all goals instead of todos associated with their goals const query = { goals: {}, todos: {} }; ``` ✅ **Correction**: Nest namespaces to fetch associated entities ``` // ✅ Good: Fetch goals and their associated todos const query = { goals: { todos: {} } }; ``` Use `where` operator to filter entities: ❌ **Common mistake**: Placing `where` at the wrong level ```typescript // ❌ Bad: Filter must be inside $ const query = { goals: { where: { id: 'goal-1' }, }, }; ``` ✅ **Correction**: Place `where` inside the `$` operator ```typescript // ✅ Good: Fetch a specific goal by ID const query = { goals: { $: { where: { id: 'goal-1', }, }, }, }; ``` `where` operators support filtering entities based on associated values ❌ **Common mistake**: Incorrect syntax for filtering on associated values ``` // ❌ Bad: This will return an error! const query = { goals: { $: { where: { todos: { title: 'Go running' }, // Wrong: use dot notation instead }, }, }, }; ``` ✅ **Correction**: Use dot notation to filter on associated values ``` // ✅ Good: Find goals that have todos with a specific title const query = { goals: { $: { where: { 'todos.title': 'Go running', }, }, todos: {}, }, }; ``` Use `or` inside of `where` to filter entities based on any criteria. ❌ **Common mistake**: Incorrect syntax for `or` and `and` ```typescript // ❌ Bad: This will return an error! const query = { todos: { $: { where: { or: { priority: 'high', dueDate: { $lt: tomorrow } }, // Wrong: 'or' takes an array }, }, }, }; ``` ✅ **Correction**: Use an array for `or` and `and` operators ```typescript // ✅ Good: Find todos that are either high priority OR due soon const query = { todos: { $: { where: { or: [{ priority: 'high' }, { dueDate: { $lt: tomorrow } }], }, }, }, }; ``` Using `$gt`, `$lt`, `$gte`, or `$lte` is supported on indexed attributes with checked types: ❌ **Common mistake**: Using comparison on non-indexed attributes ```typescript // ❌ Bad: Attribute must be indexed for comparison operators const query = { todos: { $: { where: { nonIndexedAttr: { $gt: 5 }, // Will fail if attr isn't indexed }, }, }, }; ``` ✅ **Correction**: Use comparison operators on indexed attributes ```typescript // ✅ Good: Find todos that take more than 2 hours const query = { todos: { $: { where: { timeEstimate: { $gt: 2 }, }, }, }, }; // Available operators: $gt, $lt, $gte, $lte ``` Use `limit` and/or `offset` for simple pagination: ❌ **Common mistake**: Using limit in nested namespaces ```typescript // ❌ Bad: Limit only works on top-level namespaces. This will return an error! const query = { goals: { todos: { $: { limit: 5 }, // This won't work }, }, }; ``` ✅ **Correction**: Use limit on top-level namespaces ```typescript // ✅ Good: Get first 10 todos const query = { todos: { $: { limit: 10, }, }, }; // ✅ Good: Get next 10 todos const query = { todos: { $: { limit: 10, offset: 10, }, }, }; ``` Use the `order` operator to sort results ❌ **Common mistake**: Using `orderBy` instead of `order` ```typescript // ❌ Bad: `orderBy` is not a valid operator. This will return an error! const query = { todos: { $: { orderBy: { serverCreatedAt: 'desc', }, }, }, }; ``` ✅ **Correction**: Use `order` to sort results ```typescript // ✅ Good: Sort by creation time in descending order const query = { todos: { $: { order: { serverCreatedAt: 'desc', }, }, }, }; ``` ❌ **Common mistake**: Ordering non-indexed fields ```typescript // ❌ Bad: Field must be indexed for ordering const query = { todos: { $: { order: { nonIndexedField: 'desc', // Will fail if field isn't indexed }, }, }, }; ``` ## Common mistakes with Instant on the backend Use `db.query` in the admin SDK instead of `db.useQuery`. It is an async API without loading states. We wrap queries in try catch blocks to handle errors. Unlike the client SDK, queries in the admin SDK bypass permission checks ❌ **Common mistake**: Using `db.useQuery` in the admin SDK ```javascript // ❌ Bad: Don't use useQuery on the server const { data, isLoading, error } = db.useQuery({ todos: {} }); // Wrong approach! ``` ✅ **Correction**: Use `db.query` in the admin SDK ```javascript // ✅ Good: Server-side querying const fetchTodos = async () => { try { const data = await db.query({ todos: {} }); const { todos } = data; console.log(`Found ${todos.length} todos`); return todos; } catch (error) { console.error('Error fetching todos:', error); throw error; } }; ``` ## Common mistakes with auth InstantDB does not provide built-in username/password authentication. ❌ **Common mistake**: Using password-based authentication in client-side code ✅ **Correction**: Use Instant's magic code or OAuth flows instead in client-side code If you need traditional password-based authentication, you must implement it as a custom auth flow using the Admin SDK. # Getting started How to use Instant with React Instant is the easy to use backend for your frontend. With Instant you can build delightful apps in less than 10 minutes. Follow the quick start below to **build a live app!** ## Automatic Setup With Create Instant App The fastest way to get started with Instant is to use `npx create-instant-app` to scaffold a new project with Instant already set up. To get started with Next.JS run: ```bash npx create-instant-app --next ``` ## Manual Setup To create a new Next project, fire up your terminal and run the following: ```shell npx create-next-app instant-demo --tailwind --yes cd instant-demo npm run dev ``` Add the InstantDB React Library: ```shell npm i @instantdb/react ``` Setup and connect your Instant app. This will log you in if you are not logged in already, then create a schema and permissions file, and update your `.env` file. ```shell npx instant-cli init ``` Create a database client in `src/lib/db.ts`: ```ts import { init } from '@instantdb/react'; import schema from '../instant.schema'; export const db = init({ appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!, schema, useDateObjects: true, }); ``` You're now ready to make queries and transactions to your database! ### Creating a To-Do List App Let's add a "todo" entity to our schema file at `src/instant.schema.ts`: ```ts import { i } from '@instantdb/react'; const _schema = i.schema({ entities: { $files: i.entity({ path: i.string().unique().indexed(), url: i.string(), }), $users: i.entity({ email: i.string().unique().indexed().optional(), imageURL: i.string().optional(), type: i.string().optional(), }), todos: i.entity({ text: i.string(), done: i.boolean(), createdAt: i.date(), }), }, links: { $usersLinkedPrimaryUser: { forward: { on: '$users', has: 'one', label: 'linkedPrimaryUser', onDelete: 'cascade', }, reverse: { on: '$users', has: 'many', label: 'linkedGuestUsers', }, }, }, rooms: {}, }); //... ``` Push the schema: ```shell npx instant-cli push ``` Replace the content of `src/app/page.tsx` with the following: ```typescript "use client"; import schema from "@/instant.schema"; import { db } from "@/lib/db"; import { id, i, init, InstaQLEntity } from "@instantdb/react"; type Todo = InstaQLEntity; function App() { // Read Data const { isLoading, error, data } = db.useQuery({ todos: {} }); if (isLoading) { return; } if (error) { return
Error: {error.message}
; } const { todos } = data; return (

todos

Open another tab to see todos update in realtime!
); } // Write Data // --------- function addTodo(text: string) { db.transact( db.tx.todos[id()].update({ text, done: false, createdAt: Date.now(), }), ); } function deleteTodo(todo: Todo) { db.transact(db.tx.todos[todo.id].delete()); } function toggleDone(todo: Todo) { db.transact(db.tx.todos[todo.id].update({ done: !todo.done })); } function deleteCompleted(todos: Todo[]) { const completed = todos.filter((todo) => todo.done); const txs = completed.map((todo) => db.tx.todos[todo.id].delete()); db.transact(txs); } function toggleAll(todos: Todo[]) { const newVal = !todos.every((todo) => todo.done); db.transact( todos.map((todo) => db.tx.todos[todo.id].update({ done: newVal })), ); } // Components // ---------- function ChevronDownIcon() { return ( ); } function TodoForm({ todos }: { todos: Todo[] }) { return (
{ e.preventDefault(); const input = e.currentTarget.input as HTMLInputElement; addTodo(input.value); input.value = ""; }} >
); } function TodoList({ todos }: { todos: Todo[] }) { return (
{todos.map((todo) => (
toggleDone(todo)} />
{todo.done ? ( {todo.text} ) : ( {todo.text} )}
))}
); } function ActionBar({ todos }: { todos: Todo[] }) { return (
Remaining todos: {todos.filter((todo) => !todo.done).length}
); } export default App; ``` Go to `localhost:3000`, and huzzah 🎉 You've got a fully functional todo list running! ## Next Steps Want to dive deeper on how this todo app works? Check out our step-by-step [Todo List Tutorial](/examples/todos). In this tutorial we walk through how to build the above todo list app from scratch, and explain how the queries and transactions work in more detail. You can also check out the [Working with data](/docs/init) section to learn more Instant concepts. As you get more familiar with Instant, check out our [Recommended Workflow](/docs/recommended-workflow) docs for using Instant in your projects. # Getting started with React Native How to use Instant with React Native You can use Instant in React Native projects too! Below is an example using Expo. Open up your terminal and do the following: ```shell # Create an app with expo npx create-expo-app instant-rn-demo cd instant-rn-demo # Install instant npm i @instantdb/react-native # Install peer dependencies npm i @react-native-async-storage/async-storage @react-native-community/netinfo react-native-get-random-values ``` Now open up `app/(tabs)/index.tsx` in your favorite editor and replace the entirety of the file with the following code. ```tsx import { init, i, InstaQLEntity } from '@instantdb/react-native'; import { View, Text, Button, StyleSheet } from 'react-native'; // Instant app const APP_ID = '__APP_ID__'; // Optional: You can declare a schema! const schema = i.schema({ entities: { colors: i.entity({ value: i.string(), }), }, }); type Color = InstaQLEntity; const db = init({ appId: APP_ID, schema }); const selectId = '4d39508b-9ee2-48a3-b70d-8192d9c5a059'; function App() { const { isLoading, error, data } = db.useQuery({ colors: { $: { where: { id: selectId } }, }, }); if (isLoading) { return ( Loading... ); } if (error) { return ( Error: {error.message} ); } return
; } function Main(props: { color?: Color }) { const { value } = props.color || { value: 'lightgray' }; return ( Hi! pick your favorite color {['green', 'blue', 'purple'].map((c) => { return ( ))} ); } function ActionBar({ todos }: { todos: Todo[] }) { return (
Remaining todos: {todos.filter((todo) => !todo.done).length}
); } ``` Go to `localhost:3000`, and huzzah 🎉 You've got a fully functional todo list running! ## Next Steps Want to dive deeper on how this todo app works? Check out our step-by-step [Todo List Tutorial](/examples/todos). In this tutorial we walk through how to build a todo list app from scratch, and explain how the queries and transactions work in more detail. For the advanced use case of integrating with TanStack Query and enabling SSR, refer to our [tanstack-start-with-tanstack-query](https://github.com/instantdb/instant/tree/main/examples/tanstack-start-with-tanstack-query) example. It can be scaffolded using `npx create-instant-app -b tanstack-start-with-tanstack-query`. You can also check out the [Working with data](/docs/init) section to learn more Instant concepts. As you get more familiar with Instant, check out our [Recommended Workflow](/docs/recommended-workflow) docs for using Instant in your projects. # Create Instant App Use create-instant-app to scaffold a new Instant project Once you know the basics of Instant, you may find it useful to be able to quickly scaffold a new Instant project. We built `create-instant-app` to do just that. We currently offer templates for Next.js, Expo, and Vanilla Typescript. Follow the quick start below to give it a spin! ## Quick start If you haven't already, authenticate with the [Instant CLI](/docs/cli) in your terminal. ```shell npx instant-cli login ``` This will open a browser window where you can log in or sign up for an account. Once you've authenticated, any app you create with `create-instant-app` will be associated with your Instant account! After authenticating you run the following command to scaffold a new Instant app. ```shell npx create-instant-app instant-demo ``` Run the dev server to see your new app in action! ```shell cd instant-demo npm run dev ``` Huzzah! 🎉 You now have a brand new Instant project to play around with! ## One-shot with Claude Code Got Claude Code? You can use it to one-shot a full-stack Instant app! Use `create-instant-app` with the `--ai` flag and you'll be prompted to describe the app you want to build. Give it a try! You can think of this as a one-shot app builder in the terminal. Right now this only works with Next.js and Expo. We're keen to improve this feature so if you have any feedback please let us know below or on [Discord](https://discord.com/invite/VU53p7uQcE)! # Recommended Workflow How to develop with Instant At a high level, here is the recommended workflow for developing with Instant: 1. Authenticate with Instant in your terminal via `npx instant-cli login`. 1. Create new projects via `npx create-instant-app`. 1. Push changes to your schema and permissions via `npx instant-cli push`. 1. Use the [Sandbox](https://www.instantdb.com/dash?t=sandbox) to debug queries, transactions, and permissions. 1. When you're ready for production, [restrict creating](/docs/patterns#restrict-creating-new-attributes) new attributes. 1. If you need more help, check out our [patterns page](/docs/patterns) for common recipes or drop us a line on our [Discord](https://discord.com/invite/VU53p7uQcE). ## Authenticating with Instant in your terminal Use the [Instant CLI](/docs/instant-cli) to authenticate with Instant in your terminal. After authenticating you'll be able to create new projects and have them associated with your Instant account. This will also enable you to push and pull changes to your projects. To authenticate, run: ``` npx instant-cli login ``` This will open a browser window where you can log in or sign up for an account. ## Starting a new project After authenticating, you can create a new project with [create-instant-app](/docs/create-instant-app). This will give you some starter code, and set up rules for your LLM agent. If your agent supports it, we also recommend setting up the [Instant MCP Server](/docs/using-llms#instant-mcp-server). The default rules cover the basics of using InstantDB. But if you want to add more docs for specific functionality, you can append `.md` to the end of any doc page URL to get the raw markdown. For example, here are the docs for [adding auth](/docs/auth/magic-codes.md). ## Updating schema and permissions As your project evolves, you'll likely need to update your schema and permissions. We recommend using the Instant CLI to do this. You can make edits to your local schema and permission files, and then run `npx instant-cli push` to push changes to your project. If you prefer a GUI, you can also make changes via the explorer in the [Instant dashboard](https://www.instantdb.com/dash?t=explorer). To pull these changes into your local files, run `npx instant-cli pull`. ## Debugging queries, transactions, and permissions If you're not sure why a query or transaction isn't working, or if you're running into permission issues, you can use the [Sandbox](https://www.instantdb.com/dash?t=sandbox) to help you debug. The sandbox is a REPL-like environment that lets you run queries and transactions against your project. It serves two goals: 1. Lets you run queries and transactions quickly. 2. Gives you debug info to inspect permission checks and performance. Some examples of debug info you can see in the sandbox: - Raw output from queries and transactions - Permission check results for all entities returned by a query - Permission check results for each tx operation in a transaction - How long queries and transactions take to run ### Dealing with timeout errors InstantDB has the following timeouts for queries and transactions: - 5 seconds for queries and transactions with the client SDK. - 30 seconds for queries and transactions in the admin SDK and sandbox. We set these timeouts intentionally for performance and reliability. We do not allow timeouts to be configured. Sometimes fixing a timeout is as simple as adding an index. Other times you'll need to iterate to identify the bottleneck. Some common causes of timeouts: - Missing an index - Fetching or transacting too much data - Expensive `where` clauses - Expensive permission rules that traverse a lot of data Set up test code in your sandbox and run experiments to get under the timeout limit. From there you can apply the same changes to your app code. ### Dealing with permission errors The sandbox is one of the best tools for debugging permission errors. Whenever you run a query or transaction in the sandbox, you can see all the permission checks that were run, and whether they passed or failed. If it's unclear why a permission is returning false, re-run the transaction with the permission broken down into smaller pieces. For example, if you have a permission rule like ``` "view": "auth.id in data.ref('members.id')" ``` It can be helpful to re-run the sandbox with the permission rule changed to ``` "view": "data.ref('members.id')" ``` This will show you the output of just that part of the rule. You can use this technique to iterate on complex permission rules until you find the part that is causing the permission to return false. ## Going to production Huzzah, you're ready for prime time! When you go to production, be sure to [restrict creating](/docs/patterns#restrict-creating-new-attributes) new attributes. You can also consider setting up [separate apps](/docs/patterns#managing-local-vs-production-apps) for local development and production. ## Best practices and getting help We highly recommend going through our docs to understand how Instant works. We tried our best to keep them delightful and example-driven! We've also made a [patterns page](/docs/patterns) with common recipes for using InstantDB. If you still have questions, feel free to drop us a line on our [Discord](https://discord.com/invite/VU53p7uQcE). # Using Instant with LLMs How to use Instant with LLMs You can supercharge your Instant experience by using it with LLMs. Just add our Instant rules and your off to the races! ## Instant Skill The fastest way to add rules to your LLM tool is to add the Instant skill. ```text npx skills add instantdb/skills ``` This will give your agent the context it needs to work with InstantDB. You can verify you set up the rules correctly by asking your LLM "How do you make queries and transactions in InstantDB?" If everything is set up correctly, you should see a response with information about `db.useQuery` and `db.transact` If you'd prefer to manually install the rules instead see the section below. ## Instant Rules We've created a set of rules to help LLMs understand how Instant works. If you start a new project with `create-instant-app` you'll get these rules automatically. If you have an existing project you can add the rules manually by reading below. ### Cursor [Save these rules](/llm-rules/cursor-rules.md) at the root of your project in `.cursor/rules/instant.mdc` You may need to restart Cursor for them to take effect. When using Cursor we recommend turning off "Auto" and using at least Claude Sonnet 4 ### Claude Code [Save these rules](/llm-rules/AGENTS.md) at the root of your project as `CLAUDE.md` If you already had Claude running, restart it for the rules to take effect. ### Codex [Save these rules](/llm-rules/AGENTS.md) at the root of your project as `AGENTS.md` If you already had Codex running, restart it for the rules to take effect. ### Gemini [Save these rules](/llm-rules/AGENTS.md) at the root of your project as `GEMINI.md` If you already had Gemini running, restart it for the rules to take effect. ### Windsurf [Save these rules](/llm-rules/windsurf-rules.md) at the root of your project in `.windsurf/rules/instant.md` You may need to restart Windsurf for them to take effect. ### Zed [Save these rules](/llm-rules/AGENTS.md) at the root of your project as `AGENTS.md` You may need to restart Zed for them to take effect. ### Other Tools [Save these rules](/llm-rules/AGENTS.md) at the root of your project as `AGENTS.md` If you want to manually add in more documentation, you can also append `.md` to the end of any doc page url to get the raw markdown ### Markdown Docs and llms.txt You can attach `.md` to the end of any doc page url to get raw markdown. This can be helpful to paste into your LLM if you're stuck on particular functionality. For example, here's the recommended docs for [adding auth](/docs/auth/magic-codes.md.md) We recommend starting with the rules files above and adding more docs as needed. If you want though you can get all our docs at once in markdown format via [llms-full.txt](https://www.instantdb.com/llms-full.txt) ## Instant MCP Server We built [`@instantdb/mcp`](https://github.com/instantdb/instant/tree/main/client/packages/mcp) to enable creating, managing, and updating your Instant apps. Combine the MCP with our rules file to build full-stack apps directly in your editor. The easiest way to get started is to use our hosted remote MCP server. Use the instructions below to add the Instant MCP server to your favorite LLM editor or tool. When you add the MCP server, you'll be sent through an OAuth flow to grant access to your Instant Account. ### Cursor [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=InstantDB&config=eyJ1cmwiOiJodHRwczovL21jcC5pbnN0YW50ZGIuY29tL21jcCJ9) Or edit your `~/.cursor/mcp.json` directly: ```json { "mcpServers": { "instant": { "url": "https://mcp.instantdb.com/mcp" } } } ``` ### Claude Code If you're on a paid Claude plan, you can add the the server via the command line ```text claude mcp add instant -s user -t http https://mcp.instantdb.com/mcp ``` Now you can run `claude` to start Claude Code and then run `/mcp` to see your list of MCP servers. `instant` should be listed there. Select it and go through the auth flow to enable the Instant MCP server in your claude code sessions! ### Codex If you're on a paid OpenAI plan, you can add the the server via the command line Edit your `~/.codex/config.toml` to include the [`rmcp_client` feature](https://developers.openai.com/codex/mcp/): ```toml [features] rmcp_client = true ``` Tell codex to add the MCP server: ```text codex mcp add instant --url "https://mcp.instantdb.com/mcp" ``` This should load a browser to authenticate with Instant. Now run `codex` to start Codex. You can run `/mcp` to see Instant in your list. ### Gemini If you're on a paid Google AI plan, you can add the the server via the command line ```text gemini mcp add --transport http instant https://mcp.instantdb.com/mcp ``` This should load a browser to authenticate with Instant. Now run `gemini` to start Gemini. You can run `/mcp` to see Instant in your list. ### Windsurf You can add the Instant MCP server through the Windsurf UI 1. Open Windsurf Settings. 2. Under Cascade, you'll find Model Context Protocol Servers. 3. Select Add Server and paste the relevant snippet for your OS. Alternatively you can directly edit your `~/.codeium/windsurf/mcp_config.json` **MacOS/Linux** ```json { "mcpServers": { "instant": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.instantdb.com/sse"] } } } ``` **Windows** ```json { "mcpServers": { "instant": { "command": "cmd", "args": ["/c", "npx", "-y", "mcp-remote", "https://mcp.instantdb.com/sse"] } } } ``` **Windows WSL** ```json { "mcpServers": { "instant": { "command": "wsl", "args": ["npx", "-y", "mcp-remote", "https://mcp.instantdb.com/sse"] } } } ``` ### Zed Open your Zed settings and add the following ```json { "context_servers": { "instant": { "command": { "path": "npx", "args": ["-y", "mcp-remote", "https://mcp.instantdb.com/mcp"], "env": {} }, "settings": {} } } } ``` ### Other Tools For other tools that support MCP servers, you can configure Instant using either our streamable HTTP endpoint (recommended if your tool supports it) ```text https://mcp.instantdb.com/mcp ``` Or our SSE endpoint ```text https://mcp.instantdb.com/sse ``` ## Local MCP server We recommend using our hosted MCP server but we also support running [`@instantdb/mcp`](https://github.com/instantdb/instant/tree/main/client/packages/mcp) locally via `stdio`. This will avoid OAuth but requires you to manage your personal access token. ### Get your Personal Access Token If you haven't already, make sure to get a personal access token from your [Instant dashboard](https://www.instantdb.com/dash?s=personal-access-tokens) Once you have your token, you can set up the local Instant MCP server in your favorite editor with MCP support. ### Cursor/Windsurf/Cline You can set up the Instant MCP server in Cursor, Windsurf, or Cline by adding the following configuration to your MCP settings: **MacOS/Linux** ```json { "mcpServers": { "instant": { "command": "npx", "args": ["-y", "@instantdb/mcp", "--token", ""] } } } ``` **Windows** ```json { "mcpServers": { "instant": { "command": "cmd", "args": ["/c", "npx", "-y", "@instantdb/mcp", "--token", ""] } } } ``` **Windows WSL** ```json { "mcpServers": { "instant": { "command": "wsl", "args": ["npx", "-y", "@instantdb/mcp", "--token", ""] } } } ``` Replacing `` with your personal access token. Save the file and reload the editor! You should now see the Instant MCP server active and enabled! ### Zed Open your Zed settings and add the following ```json { "context_servers": { "instant": { "command": { "path": "npx", "args": ["-y", "@instantdb/mcp", "--token", ""], "env": {} }, "settings": {} } } } ``` Replacing `` with your personal access token. Save the file and reload the editor. You should now see the Instant MCP server active and enabled! ### Claude Desktop You can set up the Instant MCP server in Claude Desktop by following these steps: 1. Open the file `~/Library/Application Support/Claude/claude_desktop_config.json` 2. Add the following configuration to the `claude_desktop_config.json` file: ```json { "mcpServers": { "instant": { "command": "npx", "args": ["-y", "@instantdb/mcp", "--token", ""] } } } ``` Replacing `` with your personal access token. Save the file and restart Claude Desktop. You should now see the Instant MCP server active and enabled! ## MCP Tools Below is a list of the current tools we expose - `learn` Fetch rules files if needed to help the LLM understand InstantDB. - `get-schema` Retrieves the schema for a specific app. - `get-perms` Retrieves permission rules for an app. - `push-schema` Applies schema changes to an app. - `push-perms` Updates permission rules for an app. # Initializing Instant How to integrate Instant with your app. ## Basic Initialization The first step to using Instant in your app is to call `init`. Here is a simple example at the root of your app. ```javascript import { init } from '@instantdb/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { return
; } ``` With that, you can use `db` to [write data](/docs/instaml), [make queries](/docs/instaql), [handle auth](/docs/auth), and more! ## Typesafety If you're using typescript, `init` accepts a `schema` argument. Adding a schema provides auto-completion and typesafety for your queries and transactions. ```typescript import { init, i } from '@instantdb/react'; // Instant app const APP_ID = '__APP_ID__'; const schema = i.schema({ entities: { $files: i.entity({ path: i.string().unique().indexed(), url: i.any(), }), $users: i.entity({ email: i.string().unique().indexed(), }), todos: i.entity({ text: i.string(), done: i.boolean(), createdAt: i.number(), }), }, }); const db = init({ appId: APP_ID, schema }); ``` To learn more about writing schemas, head on over to the [Modeling your data](/docs/modeling-data) section. ## Flexible Initialization Instant maintains a single connection regardless of where or how many times you call `init` with the same app ID. This means you can safely call `init` multiple times without worrying about creating multiple connections or performance overhead. However we do recommend the pattern of exporting a reference from a utility file like so: ```typescript // lib/db.ts import { init } from '@instantdb/react'; import schema from '../instant.schema'; // Instant app const APP_ID = '__APP_ID__'; export const db = init({ appId: APP_ID, schema }); // app/page.tsx "use client"; import { db } from '../lib/db'; function App() { // do some instant magic 🪄 db.useQuery({ todos: {} }); } export default App; ``` ## Configuration Options `init` accepts a few options. For most use cases you'll want to provide `appId` and `schema`. Here are all the options you can provide: - **appId** (required): Your InstantDB application ID. This identifies your app and is used to connect to the correct backend. - **schema?**: Instant schema export from your `instant.schema.ts` file. Provide this for typesafety and auto-completion in queries, transactions, and ephemeral features. - **websocketURI?**: Custom WebSocket endpoint for real-time connections. Defaults to `'wss://api.instantdb.com/runtime/session'`. Change this for connecting to development or self-hosted instances. - **apiURI?**: Custom HTTP API endpoint for auth and storage operations. Defaults to `'https://api.instantdb.com'`. Change this for connecting to development or self-hosted instances. - **devtool?**: Controls the Instant dev tool. Defaults to `true` on localhost. Set to `false` to disable, or configure with `{ position: 'bottom-right', allowedHosts: ['localhost'] }`. - **verbose?**: Enables detailed console logging for debugging. When `true`, logs WebSocket messages and internal operations. Helpful for troubleshooting connection and sync issues. - **queryCacheLimit?**: Maximum number of query subscriptions to cache for offline mode. Defaults to `10`. Cached queries provide instant data on app reload while fresh data loads in the background. - **useDateObjects?**: When `true`, all date columns in queries will return a Javascript `Date` object. Disabled by default. # Modeling data How to model data with Instant's schema. In this section we’ll learn how to model data using Instant's schema. By the end of this document you’ll know how to: - Create namespaces and attributes - Add indexes and unique constraints - Model relationships - Lock down your schema for production We’ll build a micro-blog to illustrate; we'll have authors, posts, comments, and tags. ## Schema as Code With Instant you can define your schema and your permissions in code. If you haven't already, use the [CLI](/docs/cli) to generate an `instant.schema.ts`, and a `instant.perms.ts` file: ```shell npx instant-cli@latest init ``` The CLI will guide you through picking an Instant app and generate these files for you. ## instant.schema.ts Now we can define the data model for our blog! Open `instant.schema.ts`, and paste the following: ```typescript // instant.schema.ts import { i } from '@instantdb/react'; const _schema = i.schema({ entities: { $users: i.entity({ email: i.string().unique().indexed(), }), profiles: i.entity({ nickname: i.string(), createdAt: i.date(), }), posts: i.entity({ title: i.string(), body: i.string(), createdAt: i.date(), }), comments: i.entity({ body: i.string(), createdAt: i.date(), }), tags: i.entity({ title: i.string(), }), }, links: { postAuthor: { forward: { on: 'posts', has: 'one', label: 'author' }, reverse: { on: 'profiles', has: 'many', label: 'authoredPosts' }, }, commentPost: { forward: { on: 'comments', has: 'one', label: 'post' }, reverse: { on: 'posts', has: 'many', label: 'comments' }, }, commentAuthor: { forward: { on: 'comments', has: 'one', label: 'author' }, reverse: { on: 'profiles', has: 'many', label: 'authoredComments' }, }, postsTags: { forward: { on: 'posts', has: 'many', label: 'tags' }, reverse: { on: 'tags', has: 'many', label: 'posts' }, }, profileUser: { forward: { on: 'profiles', has: 'one', label: '$user' }, reverse: { on: '$users', has: 'one', label: 'profile' }, }, }, }); // This helps TypeScript display better intellisense type _AppSchema = typeof _schema; interface AppSchema extends _AppSchema {} const schema: AppSchema = _schema; export type { AppSchema }; export default schema; ``` Let's unpack what we just wrote. There are three core building blocks to model data with Instant: **Namespaces**, **Attributes**, and **Links**. ## 1) Namespaces Namespaces are equivalent to "tables" in relational databases or "collections" in NoSQL. In our case, these are: `$users`, `profiles`, `posts`, `comments`, and `tags`. They're all defined in the `entities` section: ```typescript // instant.schema.ts const _schema = i.schema({ entities: { posts: i.entity({ // ... }), }, }); ``` ## 2) Attributes Attributes are properties associated with namespaces. These are equivalent to a "column" in relational databases or a "field" in NoSQL. For the `posts` entity, we have the `title`, `body`, and `createdAt` attributes: ```typescript // instant.schema.ts const _schema = i.schema({ entities: { // ... posts: i.entity({ title: i.string(), body: i.string(), createdAt: i.date(), }), }, }); ``` ### Typing attributes Attributes can be typed as `i.string()`, `i.number()`, `i.boolean()`, `i.date()`, `i.json()`, or `i.any()`. `i.date()` accepts dates as either a numeric timestamp (in milliseconds) or an ISO 8601 string. `JSON.stringify(new Date())` will return an ISO 8601 string. When you type `posts.title` as a `string`: ```typescript // instant.schema.ts const _schema = i.schema({ entities: { // ... posts: i.entity({ title: i.string(), // ... }), }, }); ``` Instant will _make sure_ that all `title` attributes are strings, and you'll get the proper typescript hints to boot! ### Required constraints All attributes you define are considered _required_ by default. This constraint is enforced on the backend: Instant guarantees that every entity of that type will have a value and reports errors if you attempt to add an entity without a required attribute. ```typescript const _schema = i.schema({ entities: { posts: i.entity({ title: i.string(), // <-- required published: i.date(), // <-- required }), }, }); db.transact( db.tx.posts[id()].update({ title: 'abc', // <-- no published -- will throw }), ); ``` You can mark attribute as optional by calling `.optional()`: ```typescript const _schema = i.schema({ entities: { posts: i.entity({ title: i.string(), // <-- required published: i.date().optional(), // <-- optional }), }, }); db.transact( db.tx.posts[id()].update({ title: 'abc', // <-- no published -- still okay }), ); ``` This will also reflect in types: query results containing `posts` will show `title: string` (non-nullable) and `published: string | number | null` (nullable). You can set required on forward links, too: ```typescript postAuthor: { forward: { on: 'posts', has: 'one', label: 'author', required: true }, reverse: { on: 'profiles', has: 'many', label: 'authoredPosts' }, }, ``` Finally, for legacy attributes that are treated as required on your front-end but you are not ready to enable back-end required checks yet, you can use `.clientRequired()`. That will produce TypeScript type without `null` but will not add back-end required check: ```typescript const _schema = i.schema({ entities: { posts: i.entity({ title: i.string().clientRequired(), published: i.date().optional(), }), }, }); ``` ### Unique constraints Sometimes you'll want to introduce a unique constraint. For example, say we wanted to add friendly URL's to posts. We could introduce a `slug` attribute: ```typescript // instant.schema.ts const _schema = i.schema({ entities: { // ... posts: i.entity({ slug: i.string().unique(), // ... }), }, }); ``` Since we're going to use post slugs in URLs, we'll want to make sure that no two posts can have the same slug. If we mark `slug` as `unique`, _Instant will guarantee this constraint for us_. Unique attributes will also speed up queries that filter by that attribute. ```typescript const query = { posts: { $: { where: { // Since `slug` is unique, this query is 🚀 fast slug: 'completing_sicp', }, }, }, }; ``` ### Indexing attributes You can also use index attributes to speed up querying. An additional benefit is that indexed attributes can be used with comparison operators for where queries like `$gt`, `$lt`, `$gte`, and `$lte` and can be used in `order` clauses. Suppose we wanted to query for products less than $100 and order by price. First we make sure that the `price` attribute is indexed: ```typescript // instant.schema.ts const _schema = i.schema({ entities: { // ... products: i.entity({ price: i.number().indexed(), // 🔥, // ... }), }, }); ``` And now we can use `$lt` and `order` in our query: ```typescript const query = { products: { $: { where: { price: { $lt: 100 }, }, order: { price: 'desc', }, }, }, }; ``` Even if you're not using comparison operators or order clauses, indexing attributes can still speed up queries that filter by that attribute. ## 3) Links Links connect two namespaces together. When you define a link, you define it both in the 'forward', and the 'reverse' direction. For example: ```typescript postAuthor: { forward: { on: "posts", has: "one", label: "author" }, reverse: { on: "profiles", has: "many", label: "authoredPosts" }, } ``` This links `posts` and `profiles` together: - `posts.author` links to _one_ `profiles` entity - `profiles.authoredPosts` links back to _many_ `posts` entities. Since links are defined in both directions, you can query in both directions too: ```typescript // This queries all posts with their author const query1 = { posts: { author: {}, }, }; // This queries profiles, with all of their authoredPosts! const query2 = { profiles: { authoredPosts: {}, }, }; ``` Links can have one of four relationship types: `many-to-many`, `many-to-one`, `one-to-many`, and `one-to-one` Our micro-blog example has the following relationship types: - **One-to-one** between `profiles` and `$users` - **One-to-many** between `posts` and `profiles` - **One-to-many** between `comments` and `posts` - **One-to-many** between `comments` and `profiles` - **Many-to-many** between `posts` and `tags` ### Cascade Delete Links defined with `has: "one"` can set `onDelete: "cascade"`. In this case, when the profile entity is deleted, all post entities will be deleted too: ```typescript postAuthor: { forward: { on: "posts", has: "one", label: "author", onDelete: "cascade" }, reverse: { on: "profiles", has: "many", label: "authoredPosts" }, } // this will delete profile and all linked posts db.tx.profiles[user_id].delete(); ``` Without `onDelete: "cascade"`, deleting a profile would simply delete the links but not delete the underlying posts. If you prefer to model links in other direction, you can do it, too: ``` postAuthor: { forward: { on: "profiles", has: "many", label: "authoredPosts" }, reverse: { on: "posts", has: "one", label: "author", onDelete: "cascade" }, } ``` ## Publishing your schema Now that you have your schema, you can use the CLI to `push` it to your app: ```shell npx instant-cli@latest push schema ``` The CLI will look at your app in production, show you the new columns you'd create, and run the changes for you! ``` Checking for an Instant SDK... Found @instantdb/react in your package.json. Found NEXT_PUBLIC_INSTANT_APP_ID: ***** Planning schema... The following changes will be applied to your production schema: ADD ENTITY profiles.id ADD ENTITY posts.id ADD ENTITY comments.id ADD ENTITY tags.id ADD ATTR profiles.nickname :: unique=false, indexed=false ADD ATTR profiles.createdAt :: unique=false, indexed=false ADD ATTR posts.title :: unique=false, indexed=false ADD ATTR posts.slug :: unique=true, indexed=false ADD ATTR posts.body :: unique=false, indexed=false ADD ATTR posts.createdAt :: unique=false, indexed=true ADD ATTR comments.body :: unique=false, indexed=false ADD ATTR comments.createdAt :: unique=false, indexed=false ADD ATTR tags.title :: unique=false, indexed=false ADD LINK posts.author <=> profiles.authoredPosts ADD LINK comments.post <=> posts.comments ADD LINK comments.author <=> profiles.authoredComments ADD LINK posts.tags <=> tags.posts ADD LINK profiles.$user <=> $users.profile ? OK to proceed? yes Schema updated! ``` ## Use schema for typesafety You can also use your schema inside `init`: ```typescript import { init } from '@instantdb/react'; import schema from '../instant.schema.ts'; const db = init({ appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!, schema, }); ``` When you do this, all [queries](/docs/instaql) and [transactions](/docs/instaml) will come with typesafety out of the box. If you haven't used the CLI to push your schema yet, no problem. Any time you write `transact`, we'll automatically create missing entities for you. ## Update or Delete attributes You can always modify or delete attributes after creating them. **You can't use the CLI to do this yet, but you can use the dashboard.** Say we wanted to rename `posts.createdAt` to `posts.publishedAt`: 1. Go to your [Dashboard](https://instantdb.com/dash) 2. Click "Explorer" 3. Click "posts" 4. Click "Edit Schema" 5. Click `createdAt` You'll see a modal that you can use to rename the attribute, index it, or delete it: ## Secure your schema with permissions In the earlier sections we mentioned that new `entities` and `attributes` can be created on the fly when you call `transact`. This can be useful for development, but you may not want this in production. To prevent changes to your schema on the fly, simply add these permissions to your app. ```typescript // instant.perms.ts import type { InstantRules } from '@instantdb/react'; const rules = { attrs: { allow: { $default: 'false', }, }, } satisfies InstantRules; export default rules; ``` Once you push these permissions to production: ```bash npx instant-cli@latest push perms ``` ``` Checking for an Instant SDK... Found @instantdb/react in your package.json. Found NEXT_PUBLIC_INSTANT_APP_ID: ***** Planning perms... The following changes will be applied to your perms: -null +{ + attrs: { + allow: { + $default: "false" + } + } +} OK to proceed? yes[21 Permissions updated! ``` You'll still be able to make changes in the explorer or with the CLI, but client-side transactions that try to modify your schema will fail. This means your schema is safe from unwanted changes! --- **If you've made it this far, congratulations! You should now be able to fully customize and lock down your data model. Huzzah!** # Writing data How to write data with Instant using InstaML. Instant uses a **Firebase-inspired** interface for mutations. We call our mutation language **InstaML** ## Creating data We use the `create` action to create entities: ```typescript import { init, id } from '@instantdb/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); // transact! 🔥 db.transact(db.tx.goals[id()].create({ title: 'eat' })); ``` This creates a new `goal` with the following properties: - It's identified by a randomly generated id via the `id()` function. - It has an attribute `title` with value `eat`. You can store `strings`, `numbers`, `booleans`, `arrays`, and `objects` as values. You can also generate values via functions. Below is an example for picking a random goal title. ```javascript db.transact( db.tx.goals[id()].create({ title: ['eat', 'sleep', 'hack', 'repeat'][Math.floor(Math.random() * 4)], }), ); ``` ## Update data The `update` action is used for updating entities. Suppose we had created the following goal ```javascript const eatId = id(); db.transact( db.tx.goals[eatId].update({ priority: 'top', lastTimeEaten: 'Yesterday' }), ); ``` We eat some food and decide to update the goal. We can do that like so: ```javascript db.transact(db.tx.goals[eatId].update({ lastTimeEaten: 'Today' })); ``` This will only update the value of the `lastTimeEaten` attribute for entity `eat`. Similar to NoSQL, you don't need to use the same schema for each entity in a namespace. After creating the previous goal you can run the following: ```javascript db.transact( db.tx.goals[id()].update({ priority: 'none', isSecret: true, value: 10, aList: [1, 2, 3], anObject: { foo: 'bar' }, }), ); ``` `update` function works as create or update depending on whether the entity already exists or not (so called "upsert" mode). If entity doesn’t exist yet, calling `update` will create it, otherwise it will update. To force “strict update” mode, pass `{ upsert: false }` option: ```javascript db.transact( db.tx.goals[eatId].update({ lastTimeEaten: 'Today' }, { upsert: false }), ); ``` ## Merge data When you `update` an attribute, you overwrite it. This is fine for updating values of strings, numbers, and booleans. But if you use `update` to overwrite json objects you may encounter two problems: 1. You lose any data you didn't specify. 2. You risk clobbering over changes made by other clients. For example, imagine we had a `game` entity, that stored a `state` of favorite colors: ```javascript // User 1 saves {'0-0': 'red'} db.transact(db.tx.games[gameId].update({ state: { '0-0': 'red' } })); // User 2 saves {'0-1': 'blue'} db.transact(db.tx.games[gameId].update({ state: { '0-1': 'blue' } })); // 🤔 Uh oh! User 2 overwrite User 1: // Final State: {'0-1': 'blue' } ``` To make working with deeply-nested, document-style JSON values a breeze, we created `merge`. Similar to [lodash's `merge` function](https://lodash.com/docs/4.17.15#merge), `merge` allows you to specify the slice of data you want to update: ```javascript // User 1 saves {'0-0': 'red'} db.transact(db.tx.games[gameId].merge({ state: { '0-0': 'red' } })); // User 2 saves {'0-1': 'blue'} db.transact(db.tx.games[gameId].merge({ state: { '0-1': 'blue' } })); // ✅ Wohoo! Both states are merged! // Final State: {'0-0': 'red', '0-1': 'blue' } ``` `merge` only merges objects. Calling `merge` on **arrays, numbers, or booleans** will overwrite the values. Sometimes you may want to remove keys from a nested object. You can do so by calling `merge` with a key set to `null`. This will remove the corresponding property from the object. Setting a key to `undefined` will have no effect. Set the key to `null` to remove the property. ```javascript // State: {'0-0': 'red', '0-1': 'blue' } db.transact(db.tx.games[gameId].merge({ state: { '0-1': null } })); // New State! {'0-0': 'red' } ``` ## Delete data The `delete` action is used for deleting entities. ```javascript db.transact(db.tx.goals[eatId].delete()); ``` You can generate an array of `delete` txs to delete all entities in a namespace ```javascript const { isLoading, error, data } = db.useQuery({ goals: {} }); const { goals } = data; // ... db.transact(goals.map((g) => db.tx.goals[g.id].delete())); ``` Calling `delete` on an entity also deletes its associations. So no need to worry about cleaning up previously created links. ## Link data `link` is used to create associations. Suppose we create a `goal` and a `todo`. ```javascript db.transact([ db.tx.todos[workoutId].update({ title: 'Go on a run' }), db.tx.goals[healthId].update({ title: 'Get fit!' }), ]); ``` We can associate `healthId` with `workoutId` like so: ```javascript db.transact(db.tx.goals[healthId].link({ todos: workoutId })); ``` We could have done all this in one `transact` too via chaining transaction chunks. ```javascript db.transact([ db.tx.todos[workoutId].update({ title: 'Go on a run' }), db.tx.goals[healthId] .update({ title: 'Get fit!' }) .link({ todos: workoutId }), ]); ``` You can specify multiple ids in one `link` as well: ```javascript db.transact([ db.tx.todos[workoutId].update({ title: 'Go on a run' }), db.tx.todos[proteinId].update({ title: 'Drink protein' }), db.tx.todos[sleepId].update({ title: 'Go to bed early' }), db.tx.goals[healthId] .update({ title: 'Get fit!' }) .link({ todos: [workoutId, proteinId, sleepId] }), ]); ``` Links are bi-directional. Say we link `healthId` to `workoutId` ```javascript db.transact(db.tx.goals[healthId].link({ todos: workoutId })); ``` We can query associations in both directions ```javascript const { isLoading, error, data } = db.useQuery({ goals: { todos: {} }, todos: { goals: {} }, }); const { goals, todos } = data; console.log('goals with nested todos', goals); console.log('todos with nested goals', todos); ``` ## Unlink data Links can be removed via `unlink.` ```javascript db.transact(db.tx.goals[healthId].unlink({ todos: workoutId })); ``` This removes links in both directions. Unlinking can be done in either direction so unlinking `workoutId` from `healthId` would have the same effect. ```javascript db.transact([db.tx.todos[workoutId].unlink({ goals: healthId })]); ``` We can `unlink` multiple ids too: ```javascript db.transact([ db.tx.goals[healthId].unlink({ todos: [workoutId, proteinId, sleepId] }), db.tx.goals[workId].unlink({ todos: [standupId, reviewPRsId, focusId] }), ]); ``` ## Lookup by unique attribute If your entity has a unique attribute, you can use `lookup` in place of the id to perform updates. ```javascript db.transact( db.tx.profiles.lookup('email', 'eva_lu_ator@instantdb.com').update({ name: 'Eva Lu Ator', }), ); ``` The `lookup` function takes the attribute as its first argument and the unique attribute value as its second argument. When it is used in a transaction, the updates will be applied to the entity that has the unique value. If no entity has the value, then a new entity with a random id will be created with the value. It can be used with `update`, `delete`, `merge`, `link`, and `unlink`. ## Lookups in links When used with links, it can also be used in place of the linked entity's id. ```javascript db.transact( db.tx.users.lookup('email', 'eva_lu_ator@instantdb.com').link({ posts: lookup('number', 15), // using a lookup in place of the id }), ); ``` ## Transacts are atomic When you call `db.transact`, all the transactions are committed atomically. If any of the transactions fail, none of them will be committed. ## Typesafety By default, `db.transact` is permissive. When you save data, we'll create missing attributes for you: ```typescript db.tx.todos[workoutId].update({ // Instant will automatically create this attribute dueDate: Date.now() + 60 * 1000, }); ``` As your app grows, you may want to start enforcing types. When you're ready, you can start using a [schema](/docs/modeling-data). If your schema includes a `todos.dueDate` for example: ```typescript // instant.schema.ts const _schema = i.schema({ entities: { todos: i.entity({ // ... dueDate: i.date(), }), }, // ... }); // ... ``` Instant will enforce that `todos.dueDate` are actually dates, and you'll get some nice intellisense to boot: Instant also comes with a few utility types, which can help you write abstractions over `transact`. For example, say you wanted to write a custom `update` function: ```typescript // Goal myCustomUpdate('todos', { dueDate: Date.now() }); ``` You can use the `UpdateParams` utility to make sure arguments follow the schema: ```typescript import { UpdateParams } from '@instantdb/react'; import { AppSchema } from '../instant.schema.ts'; type EntityTypes = keyof AppSchema['entities']; function myCustomUpdate( etype: EType, args: UpdateParams, ) { // .. } ``` And the `LinkParams` utility do the same for links: ```typescript import { LinkParams } from '@instantdb/react'; import { AppSchema } from '../instant.schema.ts'; type EntityTypes = keyof AppSchema['entities']; function myCustomLink( etype: EType, args: LinkParams, ) { // .. } ``` To learn more about writing schemas, check out the [Modeling Data](/docs/modeling-data) section. ## Batching transactions If you have a large number of transactions to commit, you'll want to batch them to avoid hitting transaction limits and time outs. Suppose we want to create 3000 goals. Here's how we can batch them into 30 transactions of 100 goals each. ```javascript const batchSize = 100; // doing 100 txs should be pretty safe const createGoals = async (total) => { let goals = []; const batches = []; // iterate through all your goals and create batches for (let i = 0; i < total; i++) { const goalNumber = i + 1; goals.push( db.tx.goals[id()].update({ goalNumber, title: `Goal ${goalNumber}` }), ); // We have enough goals to create a batch if (goals.length >= batchSize) { batches.push(goals); goals = []; // reset goals for the next batch } } // Add any remaining goals to the last batch if (goals.length) { batches.push(goals); } // Now that you have your batches, transact them for (const batch of batches) { await db.transact(batch); } }; ``` ## Using the tx proxy object `db.tx` is a [proxy object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) which creates transaction chunks to be committed via `db.transact`. It follows the format ``` db.tx.NAMESPACE_LABEL[ENTITY_IDENTIFIER].ACTION(ACTION_SPECIFIC_DATA) ``` - `NAMESPACE_LABEL` refers to the namespace to commit (e.g. `goals`, `todos`) - `ENTITY_IDENTIFIER` is the id to look up in the namespace. This id must be a uuid and unique to the namespace. You can use the `id()` function to generate a uuid for convenience. - `ACTION` is one of `create`, `update`, `merge`, `delete`, `link`, `unlink` - `ACTION_SPECIFIC_DATA` depends on the action - `create` and `update` take in an object of information to commit - `merge` takes in an object to deep merge with the existing data - `delete` is the only action that doesn't take in any data, - `link` and `unlink` takes an object of label-entity pairs to create/delete associations # Reading data How to read data with Instant using InstaQL. Instant uses a declarative syntax for querying. It's like GraphQL without the configuration. Here's how you can query data with **InstaQL.** ## Fetch namespace One of the simplest queries you can write is to simply get all entities of a namespace. ```javascript import { init } from '@instantdb/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { // Queries! 🚀 const query = { goals: {} }; const { isLoading, error, data } = db.useQuery(query); // ... } ``` Inspecting `data`, we'll see: ```javascript console.log(data) { "goals": [ { "id": healthId, "title": "Get fit!" }, { "id": workId, "title": "Get promoted!" } ] } ``` For comparison, the SQL equivalent of this would be something like: ```javascript const data = { goals: doSQL('SELECT * FROM goals') }; ``` ## Fetch multiple namespaces You can fetch multiple namespaces at once: ```javascript const query = { goals: {}, todos: {} }; const { isLoading, error, data } = db.useQuery(query); ``` We will now see data for both namespaces. ```javascript console.log(data) { "goals": [...], "todos": [ { "id": focusId, "title": "Code a bunch" }, { "id": proteinId, "title": "Drink protein" }, ... ] } ``` The equivalent of this in SQL would be to write two separate queries. ```javascript const data = { goals: doSQL('SELECT * from goals'), todos: doSQL('SELECT * from todos'), }; ``` ## Fetch a specific entity If you want to filter entities, you can use the `where` keyword. Here we fetch a specific goal. ```javascript const query = { goals: { $: { where: { id: healthId, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "goals": [ { "id": healthId, "title": "Get fit!" } ] } ``` The SQL equivalent would be: ```javascript const data = { goals: doSQL("SELECT * FROM goals WHERE id = 'healthId'") }; ``` ## Fetch associations We can fetch goals and their related todos. ```javascript const query = { goals: { todos: {}, }, }; const { isLoading, error, data } = db.useQuery(query); ``` `goals` would now include nested `todos` ```javascript console.log(data) { "goals": [ { "id": healthId, "title": "Get fit!", "todos": [...], }, { "id": workId, "title": "Get promoted!", "todos": [...], } ] } ``` ### Comparing with SQL The SQL equivalent for this would be something along the lines of: ```javascript const query = ` SELECT g.*, gt.todos FROM goals g JOIN ( SELECT g.id, json_agg(t.*) as todos FROM goals g LEFT JOIN todos t on g.id = t.goal_id GROUP BY 1 ) gt on g.id = gt.id `; const data = { goals: doSQL(query) }; ``` Notice the complexity of this SQL query. Although fetching associations in SQL is straightforward via `JOIN`, marshalling the results in a nested structure via SQL is tricky. An alternative approach would be to write two straight-forward queries and then marshall the data on the client. ```javascript const _goals = doSQL("SELECT * from goals") const _todos = doSQL("SELECT * from todos") const data = {goals: _goals.map(g => ( return {...g, todos: _todos.filter(t => t.goal_id === g.id)} )) ``` Now compare these two approaches with `InstaQL` ```javascript const query = { goals: { todos: {}, }, }; const { isLoading, error, data } = db.useQuery(query); ``` Modern applications often need to render nested relations, `InstaQL` really starts to shine for these use cases. ## Fetch specific associations ### A) Fetch associations for filtered namespace We can fetch a specific entity in a namespace as well as it's related associations. ```javascript const query = { goals: { $: { where: { id: healthId, }, }, todos: {}, }, }; const { isLoading, error, data } = db.useQuery(query); ``` Which returns ```javascript console.log(data) { "goals": [ { "id": healthId, "title": "Get fit!", "todos": [ { "id": proteinId, "title": "Drink protein" }, { "id": sleepId, "title": "Go to bed early" }, { "id": workoutId, "title": "Go on a run" } ] } ] } ``` ### B) Filter namespace by associated values We can filter namespaces **by their associations** ```javascript const query = { goals: { $: { where: { 'todos.title': 'Code a bunch', }, }, todos: {}, }, }; const { isLoading, error, data } = db.useQuery(query); ``` Returns ```javascript console.log(data) { "goals": [ { "id": workId, "title": "Get promoted!", "todos": [ { "id": focusId, "title": "Code a bunch" }, { "id": reviewPRsId, "title": "Review PRs" }, { "id": standupId, "title": "Do standup" } ] } ] } ``` ### C) Filter associations We can also filter associated data. ```javascript const query = { goals: { todos: { $: { where: { title: 'Go on a run', }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` This will return goals and filtered todos ```javascript console.log(data) { "goals": [ { "id": healthId, "title": "Get fit!", "todos": [ { "id": workoutId, "title": "Go on a run" } ] }, { "id": workId, "title": "Get promoted!", "todos": [] } ] } ``` --- Notice the difference between these three cases. - A) Fetched all todos for goal with id `health` - B) Filtered goals with a least one todo titled `Code a bunch` - C) Fetched all goals and filtered associated todos by title `Go on a run` --- ## Inverse Associations Associations are also available in the reverse order. ```javascript const query = { todos: { goals: {}, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "todos": [ { "id": focusId, "title": "Code a bunch", "goals": [ { "id": workId, "title": "Get promoted!" } ] }, ..., ] } ``` ## Defer queries You can also defer queries until a condition is met. This is useful when you need to wait for some data to be available before you can run your query. Here's an example of deferring a fetch for todos until a user is logged in. ```javascript const { isLoading, user, error } = db.useAuth(); const { isLoading: isLoadingTodos, error, data, } = db.useQuery( user ? { // The query will run once user is populated todos: { $: { where: { 'owner.id': user.id, }, }, }, } : // Otherwise skip the query, which sets `isLoading` to true null, ); ``` **NOTE:** Passing `null` to `db.useQuery` will result in `isLoading` being true. In the example above, this means that `isLoadingTodos` will _always be true_ if the user is not logged in. ## Pagination You can limit the number of items from a top level namespace by adding a `limit` to the option map: ```javascript const query = { todos: { // limit is only supported for top-level namespaces right now // and not for nested namespaces. $: { limit: 10 }, }, }; const { isLoading, error, data, pageInfo } = db.useQuery(query); ``` Instant supports both offset-based and cursor-based pagination for top-level namespaces. ### Offset To get the next page, you can use an offset: ```javascript const query = { todos: { $: { limit: 10, // similar to `limit`, `offset` is only supported for top-level namespaces offset: 10, }, }, }; const { isLoading, error, data, pageInfo } = db.useQuery(query); ``` In a React application, your offset-based pagination code might look something like this: ```jsx const [pageNumber, setPageNumber] = React.useState(1); const pageSize = 10; const query = { todos: { $: { limit: pageSize, offset: pageSize * (pageNumber - 1), }, }, }; const { isLoading, error, data } = db.useQuery(query); // Load the next page by increasing the page number, which will // increase the offset by the page size. const loadNextPage = () => { setPageNumber(pageNumber + 1); }; // Load the previous page by decreasing the page number, which will // decrease the offset by the page size. const loadPreviousPage = () => { setPageNumber(pageNumber - 1); }; ``` ### Cursors You can also get the next page with the `endCursor` returned in the `pageInfo` map from the previous result: ```javascript const query = { todos: { $: { // These also are only supported for top-level namespaces first: 10, after: pageInfo?.todos?.endCursor, }, }, }; ``` To get the previous page, use the `startCursor` in the `before` field of the option map and ask for the `last` items: ```javascript const query = { todos: { $: { last: 10, before: pageInfo?.todos?.startCursor, }, }, }; ``` In a React application, your cursor-based pagination code might look something like this: ```jsx const pageSize = 10; const [cursors, setCursors] = React.useState({ first: pageSize }); const query = { todos: { $: { ...cursors, }, }, }; const { isLoading, error, data, pageInfo } = db.useQuery(query); const loadNextPage = () => { const endCursor = pageInfo?.todos?.endCursor; if (endCursor) { setCursors({ after: endCursor, first: pageSize }); } }; const loadPreviousPage = () => { const startCursor = pageInfo?.todos?.startCursor; if (startCursor) { setCursors({ before: startCursor, // Ask for the `last` 10 items so that we get the items just // before our startCursor last: pageSize, }); } }; ``` ### Ordering The default ordering is by the time the objects were created, in ascending order. You can change the order with the `order` key in the option map for namespaces: ```javascript const query = { todos: { $: { limit: 10, // Similar to limit, order is limited to top-level namespaces right now order: { serverCreatedAt: 'desc', }, }, }, }; ``` The `serverCreatedAt` field is a reserved key that orders by the time that the object was first persisted on the Instant backend. It can take the value 'asc' (the default) or 'desc'. You can also order by any attribute that is indexed and has a checked type. Add indexes and checked types to your attributes from the [Explorer on the Instant dashboard](/dash?t=explorer) or from the [cli](/docs/cli). ```typescript // Get the todos that are due next const query = { todos: { $: { limit: 10, where: { dueDate: { $gt: Date.now() }, }, order: { dueDate: 'asc', }, }, }, }; ``` You can use order in nested namespaces as well: ```typescript // Get goals with their associated todos ordered by due date const query = { goals: { todos: { $: { order: { dueDate: 'asc', }, }, }, }, }; ``` Order is not supported on nested attributes. So if todos had an owner you could not order todos by "owner.name". This behavior is different from `where`, which supports filtering on nested attributes. ```typescript // ❌ Order does not support nested attributes const query = { todos: { $: { order: { 'owner.name': 'asc', // Cannot order by nested attributes }, }, }, }; // ✅ Where does support filtering on nested attributes const query = { todos: { $: { where: { 'owner.name': 'alyssa.p.hacker@instantdb.com', }, }, }, }; ``` ## Advanced filtering ### Multiple `where` conditions The `where` clause supports multiple keys which will filter entities that match all of the conditions. ```javascript const query = { todos: { $: { where: { completed: true, 'goals.title': 'Get promoted!', }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "todos": [ { "id": focusId, "title": "Code a bunch", "completed": true } ] } ``` ### And The `where` clause supports `and` queries which are useful when you want to filter entities that match multiple associated values. In this example we want to find goals that have todos with the titles `Drink protein` and `Go on a run` ```javascript const query = { goals: { $: { where: { and: [ { 'todos.title': 'Drink protein' }, { 'todos.title': 'Go on a run' }, ], }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "goals": [ { "id": healthId, "title": "Get fit!" } ] } ``` ### OR The `where` clause supports `or` queries that will filter entities that match any of the clauses in the provided list: ```javascript const query = { todos: { $: { where: { or: [{ title: 'Code a bunch' }, { title: 'Review PRs' }], }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data); { "todos": [ { "id": focusId, "title": "Code a bunch" }, { "id": reviewPRsId, "title": "Review PRs" }, ] } ``` ### $in The `where` clause supports `$in` queries that will filter entities that match any of the items in the provided list. You can think of this as a shorthand for `or` on a single key. ```javascript const query = { todos: { $: { where: { title: { $in: ['Code a bunch', 'Review PRs'] }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "todos": [ { "id": focusId, "title": "Code a bunch" }, { "id": reviewPRsId, "title": "Review PRs" } ] } ``` ### Comparison operators The `where` clause supports comparison operators on fields that are indexed and have checked types. Add indexes and checked types to your attributes from the [Explorer on the Instant dashboard](/dash?t=explorer) or from the [cli with Schema-as-code](/docs/modeling-data). | Operator | Description | JS equivalent | | :------: | :----------------------: | :-----------: | | `$gt` | greater than | `>` | | `$lt` | less than | `<` | | `$gte` | greater than or equal to | `>=` | | `$lte` | less than or equal to | `<=` | ```javascript const query = { todos: { $: { where: { timeEstimateHours: { $gt: 24 }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data); { "todos": [ { "id": buildShipId, "title": "Build a starship prototype", "timeEstimateHours": 5000 } ] } ``` Dates can be stored as timestamps (milliseconds since the epoch, e.g. `Date.now()`) or as ISO 8601 strings (e.g. `JSON.stringify(new Date())`) and can be queried in the same formats: ```javascript const now = '2024-11-26T15:25:00.054Z'; const query = { todos: { $: { where: { dueDate: { $lte: now } } }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data); { "todos": [ { "id": slsFlightId, "title": "Space Launch System maiden flight", "dueDate": "2017-01-01T00:00:00Z" } ] } ``` If you try to use comparison operators on data that isn't indexed and type-checked, you'll get an error: ```javascript const query = { todos: { $: { where: { priority: { $gt: 2 } } }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(error); { "message": "Validation failed for query", "hint": { "data-type": "query", "errors": [ { "expected?": "indexed?", "in": ["priority", "$", "where", "priority"], "message": "The `todos.priority` attribute must be indexed to use comparison operators." } ], "input": { "todos": { "$": { "where": { "priority": { "$gt": 2 } } } } } } } ``` ### $ne The `where` clause supports `$ne` queries that will return entities that don't match the provided value for the field, including entities where the field is null or undefined. ```javascript const query = { todos: { $: { where: { location: { $ne: 'work' }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "todos": [ { "id": cookId, "title": "Cook dinner", "location": "home" }, { "id": readId, "title": "Read", "location": null }, { "id": napId, "title": "Take a nap" } ] } ``` ### $isNull The `where` clause supports `$isNull` queries that will filters entities by whether the field value is either null or undefined. Set `$isNull` to `true` to return entities where where the field is null or undefined. Set `$isNull` to `false` to return entities where the field is not null and not undefined. ```javascript const query = { todos: { $: { where: { location: { $isNull: false }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "todos": [ { "id": cookId, "title": "Cook dinner", "location": "home" } ] } ``` ```javascript const query = { todos: { $: { where: { location: { $isNull: true }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "todos": [ { "id": readId, "title": "Read", "location": null }, { "id": napId, "title": "Take a nap" } ] } ``` ### $like The `where` clause supports `$like` on fields that are indexed with a checked `string` type. `$like` queries will return entities that match a **case sensitive** substring of the provided value for the field. For **case insensitive** matching use `$ilike` in place of `$like`. Here's how you can do queries like `startsWith`, `endsWith` and `includes`. | Example | Description | JS equivalent | | :-----------------------: | :-------------------: | :-----------: | | `{ $like: "Get%" }` | Starts with 'Get' | `startsWith` | | `{ $like: "%promoted!" }` | Ends with 'promoted!' | `endsWith` | | `{ $like: "%fit%" }` | Contains 'fit' | `includes` | Here's how you can use `$like` to find all goals that end with the word "promoted!" ```javascript // Find all goals that end with the word "promoted!" const query = { goals: { $: { where: { title: { $like: '%promoted!' }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "goals": [ { "id": workId, "title": "Get promoted!", } ] } ``` You can use `$like` in nested queries as well ```javascript // Find goals that have todos with the word "standup" in their title const query = { goals: { $: { where: { 'todos.title': { $like: '%standup%' }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` Returns ```javascript console.log(data) { "goals": [ { "id": standupId, "title": "Perform standup!", } ] } ``` Case-insensitive matching with `$ilike`: ```javascript const query = { goals: { $: { where: { 'todos.title': { $ilike: '%stand%' }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "goals": [ { "id": standupId, "title": "Perform standup!", }, { "id": standId, "title": "Stand up a food truck.", } ] } ``` ## Select fields An InstaQL query will fetch all fields for each object. If you prefer to select the specific fields that you want your query to return, use the `fields` param: ```javascript const query = { goals: { $: { fields: ['status'], }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "goals": [ { "id": standupId, // id will always be returned even if not specified "status": "in-progress" }, { "id": standId, "status": "completed" } ] } ``` `fields` also works with nested relations: ```javascript const query = { goals: { $: { fields: ['title'], }, todos: { $: { fields: ['id'], }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "goals": [ { "id": standupId, "title": "Perform standup!", "todos": [{"id": writeJokesId}, {"id": goToOpenMicId}] }, { "id": standId, "title": "Stand up a food truck.", "todos": [{"id": learnToCookId}, {"id": buyATruckId}] } ] } ``` Using `fields` can be useful for performance optimization. It reduces the amount of data that needs to be transferred from the server and minimizes the number of re-renders in your React application if there are no changes to your selected fields. Using `fields` doesn't restrict a client from doing a full query. If you have sensitive data on your entities that you don't want to expose you'll want to use [permissions](/docs/permissions#fields) to restrict access. ## Typesafety By default, `db.useQuery` is permissive. You don't have to tell us your schema upfront, and you can write any kind of query: ```typescript const query = { goals: { todos: {}, }, }; const { isLoading, error, data } = db.useQuery(query); ``` As your app grows, you may want to start enforcing types. When you're ready you can write a [schema](/docs/modeling-data). If your schema includes `goals` and `todos` for example: ```typescript // instant.schema.ts import { i } from '@instantdb/react'; const _schema = i.schema({ entities: { goals: i.entity({ title: i.string(), }), todos: i.entity({ title: i.string(), text: i.string(), done: i.boolean(), createdAt: i.date(), dueDate: i.date(), }), }, links: { goalsTodos: { forward: { on: 'goals', has: 'many', label: 'todos' }, reverse: { on: 'todos', has: 'many', label: 'goals' }, }, }, }); // This helps TypeScript display better intellisense type _AppSchema = typeof _schema; interface AppSchema extends _AppSchema {} const schema: AppSchema = _schema; export type { AppSchema }; export default schema; ``` Instant will start giving you intellisense for your queries. For example, if you're querying for goals, you'll see that only `todos` can be associated: And if you hover over `data`, you'll see the actual typed output of your query: ### Utility Types Instant also comes with some utility types to help you use your schema in TypeScript. For example, you could define your `query` upfront: ```typescript import { InstaQLParams } from '@instantdb/react'; import { AppSchema } from '../instant.schema.ts'; // `query` typechecks against our schema! const query = { goals: { todos: {} }, } satisfies InstaQLParams; ``` Or you can define your result type: ```typescript import { InstaQLResult } from '@instantdb/react'; import { AppSchema } from '../instant.schema.ts'; type GoalsTodosResult = InstaQLResult; ``` Or you can extract a particular entity: ```typescript import { InstaQLEntity } from '@instantdb/react'; import { AppSchema } from '../instant.schema.ts'; type Todo = InstaQLEntity; ``` You can specify links relative to your entity: ```typescript type TodoWithGoals = InstaQLEntity; ``` To learn more about writing schemas, check out the [Modeling Data](/docs/modeling-data) section. ## Query once Sometimes, you don't want a subscription, and just want to fetch data once. For example, you might want to fetch data before rendering a page or check whether a user name is available. In these cases, you can use `queryOnce` instead of `useQuery`. `queryOnce` returns a promise that resolves with the data once the query is complete. Unlike `useQuery`, `queryOnce` will throw an error if the user is offline. This is because `queryOnce` is intended for use cases where you need the most up-to-date data. ```javascript const query = { todos: {} }; const { data } = await db.queryOnce(query); // returns the same data as useQuery, but without the isLoading and error fields ``` You can also do pagination with `queryOnce`: ```javascript const query = { todos: { $: { limit: 10, offset: 10, }, }, }; const { data, pageInfo } = await db.queryOnce(query); // pageInfo behaves the same as with useQuery ``` # Instant on the Backend How to use Instant on the server with the Admin SDK. You can use Instant on the server as well! This can be especially useful for running scripts, custom auth flows, or sensitive application logic. ## Admin SDK We currently offer a javascript library `@instantdb/admin` for using Instant in a non-browser context. This library is similar to our client SDK with a few tweaks. ### init ```javascript import { init, id } from '@instantdb/admin'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID, adminToken: process.env.INSTANT_APP_ADMIN_TOKEN, }); ``` Similar to `@instantdb/react`, you must `init` before doing any queries or writes. Running `init` authenticates you against our admin API. In addition to providing your `appId`, you can provide your `adminToken`. Whereas exposing your `appId` in source control is fine, it's not safe to expose your `adminToken`. Permission checks will not run for queries and writes from our admin API. Be sure to regenerate your token from your dashboard if it accidentally leaks. ## Reading and Writing Data `query` and `transact` let you read and write data as an admin. ### query ```javascript const data = await db.query({ goals: {}, todos: {} }); const { goals, todos } = data; ``` In react we use `db.useQuery` to enable "live queries", queries that will automatically update when data changes. In the admin SDK we instead use an async `db.query` function that simply fires a query once and returns a result. ### transact ```typescript const res = await db.transact([db.tx.todos[id()].update({ title: 'Get fit' })]); console.log('New todo entry made for with tx-id', res['tx-id']); ``` `db.transact` is an async function that behaves nearly identical to `db.transact` from `@instantdb/react`. It returns a `tx-id` on success. ## Subscriptions on the backend You can use `db.subscribeQuery` to subscribe to queries on the backend. This can be useful if you have backend processes that react to database changes. For example, let's say we wanted to subscribe to a `tasks` table. ### With callbacks You could pass in a callback to `db.subscribeQuery` that gets called with newly updated query results: ```typescript const sub = db.subscribeQuery({ tasks: { $: { limit: 10 } } }, (payload) => { if (payload.type === 'error') { console.log('error', error); sub.close(); } else { console.log('got data!', payload.data); } }); // When you want to close the subscription: sub.close(); ``` ### With async iterator Or if you prefer, you can skip providing a callback and use async iterators: ```typescript const sub = db.subscribeQuery({ tasks: { $: { limit: 10 } } }); for await (const payload of sub) { if (payload.type === 'error') { console.log('error', error); sub.close(); } else { console.log('data', payload.data); } } // When you want to close the subscription: sub.close(); ``` Subscriptions keep a live connection open on your backend. Be sure to close them when they’re no longer needed to avoid tying up resources unnecessarily. ## Schema `init` also accepts a schema argument: ```typescript import { init, id } from '@instantdb/admin'; import schema from '../instant.schema.ts'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID, adminToken: process.env.INSTANT_APP_ADMIN_TOKEN, schema, }); ``` If you add a schema, `db.query` and `db.transact` will come with autocompletion and typesafety out of the box. The backend will also use your schema to generate missing attributes. To learn more about writing schemas, head on over to the [Modeling your data](/docs/modeling-data) section. ## Impersonating users When you use the admin SDK, you can make _any_ query or transaction. As an admin, you bypass permissions. But, sometimes you want to make queries on behalf of your users, and would like to respect permissions. You can do this with the `db.asUser` function. ```typescript // Scope by their email const scopedDb = db.asUser({ email: 'alyssa_p_hacker@instantdb.com' }); // Or with their auth token const token = db.auth.createToken({ email: 'alyssa_p_hacker@instantdb.com' }); const scopedDb = db.asUser({ token }); // Or use the db as a guest! const scopedDb = db.asUser({ guest: true }); // Queries and transactions will run with those permissions await scopedDb.query({ logs: {} }); ``` ### Running the admin SDK in client environments Impersonation can also let you run the Admin SDK _without_ exposing an admin token. ```javascript import { init } from '@instantdb/admin'; // If you only impersonate with a user token or as a guest, // you can _skip_ admin credentials const db = init({ appId: process.env.INSTANT_APP_ID! }); // Pass in a user token to run as a particular user const userDB = db.asUser({ token: "...", }); // Or run as a guest const guestDB = db.asUser({ guest: true, }); // Queries and transactions work will work with respective permissions await userDB.query({ todos: {} }); await guestDB.query({ publicData: {} }); ``` This approach is perfect for places where you need something like the Admin SDK, but don't want to expose admin credentials. For example, what if you want to run a background daemon on a user's machine? You can't use the client SDK, because you don't need optimistic updates. You wouldn't want to provide the admin token, because this code runs on a user's machine. That's when impersonation really comes in handy. Without an `adminToken`, you must use `.asUser({ token })` or `asUser({ guest: true })` for all operations. Direct queries and transactions on the base `db` instance will fail, and you won't be able to impersonate with `asUser({ email })`. In protected environments we definitely recommend including the `adminToken`. ## Retrieve a user As an admin, you can retrieve an app user record by `email`, `id`, or `refresh_token`. You can do this with the `db.auth.getUser` function. ```typescript const user = await db.auth.getUser({ email: 'alyssa_p_hacker@instantdb.com' }); const user = await db.auth.getUser({ id: userId, }); const user = await db.auth.getUser({ refresh_token: userRefreshToken, }); ``` ## Delete a user You can also delete an app user record by `email`, `id`, or `refresh_token`. You can do this with the `db.auth.deleteUser` function. ```typescript const deletedUser = await db.auth.deleteUser({ email: 'alyssa_p_hacker@instantdb.com', }); const deletedUser = await db.auth.deleteUser({ id: userId, }); const deletedUser = await db.auth.deleteUser({ refresh_token: userRefreshToken, }); ``` Note, this _only_ deletes the user record and any associated data with cascade on delete. If there's additional data you need to clean up you'll need to do it manually: ```typescript const { goals, todos } = await db.query({ goals: { $: { where: { creator: userId } } }, todos: { $: { where: { creator: userId } } }, }); await db.transact([ ...goals.map((item) => db.tx.goals[item.id].delete()), ...todos.map((item) => tx.todos[item.id].delete()), ]); // Now we can delete the user await db.auth.deleteUser({ id: userId }); ``` ## Presence in the Backend If you use [rooms & presence](/docs/presence-and-topics), you may want to query for the data currently in a room with the admin API. This can be especially useful if you are sending a notification for example, and want to skip it if the user is already online. To do get room data from the admin API, use `db.rooms.getPresence`: ```typescript const data = await db.rooms.getPresence('chat', 'room-123'); console.log(Object.values(data)); // [{ // 'peer-id': '...', // user: { id: '...', email: 'foo@bar.com', ... }, // data: { typing: true, ... }, // }, // }]; ``` ## Sign Out The `db.auth.signOut` method allows you to log out users. You can log a user out from every session by passing in their `email`, or `id`. Or you can log a user out from a particular session by passing in a `refresh_token`: ```typescript // All sessions for this email sign out await db.auth.signOut({ email: 'alyssa_p_hacker@instantdb.com' }); // All sessions for this user id sign out const user = await db.auth.signOut({ id: userId, }); // Just sign out the session for this refresh token await db.auth.signOut({ refresh_token: userRefreshToken, }); ``` ## Custom Auth You can use the Admin SDK to create your own authentication flows. To implement custom auth flows, you would make one change in your backend, and one change in your frontend. Here's how it would look: ### 1. Backend: db.auth.createToken Create a new `sign-in` endpoint in your backend. This endpoint will use `db.auth.createToken` to generate an authentication token for the user. ```typescript app.post('/sign-in', async (req, res) => { // your custom logic for signing users in // ... // on success, create and return a token const token = await db.auth.createToken({ email }); return res.status(200).send({ token }); }); ``` `db.auth.createToken` accepts either an email or a UUID. For the UUID variant: ```typescript const token = await db.auth.createToken({ id }); ``` If a user with the provider id or email does not exist, `db.auth.createToken` will create the user for you. ### 2. Frontend: db.auth.signInWithToken Once your frontend calls your `sign-in` endpoint, it can then use the generated token and sign a user in with `db.auth.signInWithToken`. Here's a full example: ```typescript import React, { useState } from 'react'; import { init } from '@instantdb/react'; // Instant app const APP_ID = "__APP_ID__"; const db = init({ appId: APP_ID }); async function customSignIn( email: string, password: string ): Promise<{ token: string }> { const response = await fetch('your-website.com/api/sign-in', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, password }), }); const data = await response.json(); return data; } function App() { const { isLoading, user, error } = db.useAuth(); if (isLoading) { return
Loading...
; } if (error) { return
Uh oh! {error.message}
; } if (user) { return
Hello {user.email}!
; } return ; } function Login() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const handleEmailChange = (event: React.ChangeEvent) => { setEmail(event.target.value); }; const handlePasswordChange = (event: React.ChangeEvent) => { setPassword(event.target.value); }; const handleSignIn = async () => { const data = await customSignIn(email, password); // initiate your custom sign in flow db.auth.signInWithToken(data.token); // sign in with the token on success }; return (
); } ``` ## Custom magic codes We support a [magic code flow](/docs/auth) out of the box. However, if you'd like to use your own email provider to send the code, you can do this with `db.auth.generateMagicCode` function: ```typescript app.post('/custom-send-magic-code', async (req, res) => { const { code } = await db.auth.generateMagicCode(req.body.email); // Now you can use your email provider to send magic codes await sendMyCustomMagicCodeEmail(req.body.email, code); return res.status(200).send({ ok: true }); }); ``` You can also use Instant's default email provider to send a magic code with `db.auth.sendMagicCode`: ```typescript // You can trigger a magic code email in your backend with `sendMagicCode` const { code } = await db.auth.sendMagicCode(req.body.email); ``` Similarly, you can verify a magic code with `db.auth.verifyMagicCode`: ```typescript const user = await db.auth.verifyMagicCode(req.body.email, req.body.code); const token = user.refresh_token; ``` ## Authenticated Endpoints You can also use the admin SDK to authenticate users in your custom endpoints. This would have two steps: ### 1. Frontend: user.refresh_token In your frontend, the `user` object has a `refresh_token` property. You can pass this token to your endpoint: ```javascript // client import { init } from '@instantdb/react'; const db = init(/* ... */) function App() { const { user } = db.useAuth(); // call your api with `user.refresh_token` function onClick() { myAPI.customEndpoint(user.refresh_token, ...); } } ``` ### 2. Backend: auth.verifyToken You can then use `auth.verifyToken` to verify the `refresh_token` that was passed in. ```typescript app.post('/custom_endpoint', async (req, res) => { // verify the token this user passed in const user = await db.auth.verifyToken(req.headers['token']); if (!user) { return res.status(401).send('Uh oh, you are not authenticated'); } // ... }); ``` ### Syncing Auth Sometimes you want to get the logged in user in the backend without needing to explicitly pass in the refresh token from the frontend. Here's how you can do it. Instant provides a `createInstantRouteHandler` function that generates a web standard endpoint that can be used to sync the refresh token to a cookie that your server can read. To use it in NextJS: ```typescript // src/app/api/instant/route.ts import { createInstantRouteHandler } from '@instantdb/react/nextjs'; export const { POST } = createInstantRouteHandler({ appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!, }); ``` The GET and POST functions accept a [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and return a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Request) so they should be able to be used in any framework. Then, provide your mounted api url to the `init` function. ```typescript import { init } from '@instantdb/react'; export const db = init({ appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!, firstPartyPath: '/api/instant', // the endpoint that you registered the route handler at. schema, useDateObjects: true, }); ``` If using NextJS you can call getUnverifiedUserFromInstantCookie with the app id to retrieve the user in any server component, or route handler. ```typescript import { getUserFromInstantCookie } from "@instantdb/react/nextjs"; // This is a server component! export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { // Get the instant user from the cookie const user = await getUnverifiedUserFromInstantCookie(process.env.NEXT_PUBLIC_INSTANT_APP_ID!); return ( {children} ); } ``` # Patterns Common patterns for working with InstantDB. Below are some common patterns for working with InstantDB. We'll add more patterns over time and if you have a pattern you'd like to share, please feel free to submit a PR for this page. ## You can expose your app id to the client. Similar to Firebase, the app id is a unique identifier for your application. If you want to secure your data, you'll want to add [permissions](/docs/permissions) for the app. ## Restrict creating new attributes. When you're ready to lock down your schema, you can restrict creating a new attribute by adding this to your app's [permissions](/dash?t=perms) ```json { "attrs": { "allow": { "$default": "false" } } } ``` This will prevent any new attributes from being created. ## Attribute level permissions When you query a namespace, it will return all the attributes for an entity. You can use the [`fields`](/docs/instaql#select-fields) clause to restrict which attributes are returned from the server but this will not prevent a client from doing another query to get the full entity. You can set special permission rules for [`fields`](/docs/instaql/#fields), to make sure only certain attributes are visible to certain users. ## Find entities with no links. If you want to find entities that have no links, you can use the `$isNull` query filter. For example, if you want to find all posts that are not linked to an author you can do ```javascript const { isLoading, error, data } = db.useQuery({ posts: { $: { where: { 'author.id': { $isNull: true, }, }, }, }, }); ``` ## Setting limits via permissions. If you want to limit the number of entities a user can create, you can do so via permissions. Here's an example of limiting a user to creating at most 2 todos. First the [schema](/docs/modeling-data): ```typescript // instant.schema.ts // Here we define users, todos, and a link between them. import { i } from '@instantdb/core'; const _schema = i.schema({ entities: { $users: i.entity({ email: i.string().unique().indexed(), }), todos: i.entity({ label: i.string(), }), }, links: { userTodos: { forward: { on: 'todos', has: 'one', label: 'owner', }, reverse: { on: '$users', has: 'many', label: 'ownedTodos', }, }, }, }); // This helps TypeScript display nicer intellisense type _AppSchema = typeof _schema; interface AppSchema extends _AppSchema {} const schema: AppSchema = _schema; export type { AppSchema }; export default schema; ``` Then the [permissions](/docs/permissions): ```typescript import type { InstantRules } from '@instantdb/react'; // instant.perms.ts // And now we reference the `owner` link for todos to check the number // of todos a user has created. // (Note): Make sure the `owner` link is already defined in the schema. // before you can reference it in the permissions. const rules = { todos: { allow: { create: "size(data.ref('owner.ownedTodos.id')) <= 2", }, }, } satisfies InstantRules; export default rules; ``` ## Listen to InstantDB connection status. Sometimes you want to let clients know when they are connected or disconnected to the DB. You can use `db.subscribeConnectionStatus` in vanilla JS or `db.useConnectionStatus` in React to listen to connection changes ```javascript // Vanilla JS const unsub = db.subscribeConnectionStatus((status) => { const statusMap = { connecting: 'authenticating', opened: 'authenticating', authenticated: 'connected', closed: 'closed', errored: 'errored', }; const connectionState = statusMap[status] || 'unexpected state'; console.log('Connection status:', connectionState); }); // React/React Native function App() { const statusMap = { connecting: 'authenticating', opened: 'authenticating', authenticated: 'connected', closed: 'closed', errored: 'errored', }; const status = db.useConnectionStatus(); const connectionState = statusMap[status] || 'unexpected state'; return
Connection state: {connectionState}
; } ``` ## Using Instant via CDN If you have a plain html page or avoid using a build step, you can use InstantDB via a CDN through [unpkg](https://www.unpkg.com/@instantdb/core/). ```jsx ``` ## Making Local ids Sometimes you need an identifier that stays the same between refreshes. A "local id" of sorts. Local ids are especially useful for features like "guest" mode. You need an identifier for the user who is accessing the service, but they haven't signed up yet. Well, you can use a `localId` for that. To generate one, use `db.getLocalId`: ```js import { init } from '@instantdb/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); const id = await db.getLocalId('guest'); console.log(id, 'stays the same even if you refresh'); ``` Or a handy hook if you're inside React: ```js import { init } from '@instantdb/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { const id = db.useLocalId('guest'); if (!id) return; console.log(id, 'stays the same even if you refresh'); } ``` Note: passing in different arguments will produce different ids: ```js const id1 = db.useLocalId('device'); const id2 = db.useLocalId('session'); console.log( id1, id2, 'are different. But each will stay the same even if you refresh', ); ``` Once you have an ID, you can pass it around in your transactions and queries, and use them in [ruleParams](/docs/permissions#rule-params). ## Making admin queries work with NextJS Caching NextJS caches fetch requests and lets you revalidate them. [`adminDB.query`](/docs/backend#query) uses fetch under the hood, so NextJS caching will work by default. If you want to finely control how the query caches, you can pass in the same kind of [fetch options](https://nextjs.org/docs/app/building-your-application/caching#fetch) for NextJS. For example, to revalidate a query every hour: ```js await adminDB.query( { goals: {} }, { fetchOpts: { next: { revalidate: 3600 }, }, }, ); ``` Or to set a specific tag: ```js await adminDB.query( { goals: {} }, { fetchOpts: { next: { tags: ['goals:all'] }, }, }, ); ``` ## Composite keys Sometimes an item is unique by two or more attributes. For example, consider a `location`: it's unique by `latitude` _and_ `longitude`. How can you enforce this uniqueness in Instant? We don't have composite keys built-in, but you can manage them by creating a composite column. For example, you can make sure `locations` are unique by adding a `latLong` column: ```js import { i } from '@instantdb/core'; const _schema = i.schema({ entities: { // ... locations: i.entity({ latitude: i.number().indexed(), longitude: i.number().indexed(), latLong: i.string().unique() // <-- our composite column }), }, ``` We can then set `latLong` in our updates: ```js function createLocation({ latitude, longitude }) { db.transact( db.tx.locations[id()].update({ latitude, longitude, latLong: `${latitude}_${longitude}`, }), ); } ``` Now, any locations with the same latitude and longitude will throw a uniqueness error. To make sure that `latLong` _always_ matches `latitude` and `longitude`, you can add a rule in your permissions: ```js const rules = { locations: { allow: { create: "(data.latitude + '_' + data.longitude) == data.latLong", update: "(newData.latitude + '_' + newData.longitude) == newData.latLong", }, }, }; ``` ## Managing local vs production apps You may want to have separate Instant apps for local development and production. The way to do this right now is to have two separate apps, one for local and one for production. ```javascript // lib/db.ts import { init } from '@instantdb/react'; const APP_ID = process.env.NEXT_PUBLIC_INSTANT_APP_ID; export const db = init({ appId: APP_ID }); ``` Then in your environment files: ```bash # .env.local NEXT_PUBLIC_INSTANT_APP_ID=your-local-app-id # .env.production NEXT_PUBLIC_INSTANT_APP_ID=your-production-app-id ``` When developing new features that require schema or permission changes, you can follow this workflow: 1. **Push schema/perms changes locally first** ```bash npx instant-cli push --app your-local-app-id ``` 2. **Test code changes** - Verify your new code works as expected with your changes in your local environment and app. 3. **Push changes to production** ```bash npx instant-cli push --app your-production-app-id ``` 4. **Deploy code changes to prod!** - And that should be it! ## Dealing with timeouts Complicated queries or large transactions may fail due to timeouts. Right now we have a hard limit of 5 seconds for both queries and transactions. We do this to ensure real-time doesn't suffer from long-running operations. To get a sense for how long a query or transaction takes, you can use the `Sandbox` tab in the dashboard. Using `await db.query` or `await db.transact` will show you the time it took to run the operation in the console. Queries and transactions in the sandbox can run for up to 30 seconds, Once you have a sense of how long your queries and transactions take, you can iteratively optimize them. For example, you can use pagination or add indexes to speed up queries, or break up large transactions into smaller ones. ## Setting default values when making an attribute required When converting an optional attribute to a required one, you'll need to update existing entities with null values. For example, if you try to make `done` required on `todos` but some records have null values: ``` INVALID DATA adding required constraint to todos.done. First few examples: namespace | id | done ----------------|---------------------------------------|------ todos | 13a09c93-cee0-4e21-9e41-c7e08edd1649 | null todos | 043eec60-d971-4689-8e4a-099e82c251a2 | null ``` The solution is to run an admin script to backfill the null values before pushing the schema change. ```typescript // scripts/backfill-todos.ts import { init, tx } from '@instantdb/admin'; const db = init({ appId: process.env.INSTANT_APP_ID!, adminToken: process.env.INSTANT_ADMIN_TOKEN!, }); async function backfillDefaults() { // Query for entities with null/missing done const { data } = await db.query({ todos: { $: { where: { done: { $isNull: true }, }, }, }, }); // Update them with a default value const updates = data.todos.map((todo) => tx.todos[todo.id].update({ done: false }), ); if (updates.length > 0) { await db.transact(updates); console.log(`Backfilled ${updates.length} records`); } else { console.log('No records to backfill'); } } backfillDefaults(); ``` Then run the script before pushing your schema: ```bash npx tsx scripts/backfill-todos.ts && npx instant-cli push ``` This pattern gives you full control over setting default values. You can set static values as shown above, or use more complex logic to determine the default based on other attributes. # Auth Instant supports magic code, OAuth, Clerk, and custom auth. Instant comes with support for auth. We currently offer [Magic Codes](/docs/auth/magic-codes), [Guest Auth](/docs/auth/guest-auth), [Google OAuth](/docs/auth/google-oauth), [Sign In with Apple](/docs/auth/apple), [Github OAuth](/docs/auth/github-oauth), [LinkedIn OAuth](/docs/auth/linkedin-oauth), and [Clerk](/docs/auth/clerk). If you want to build your own flow, you can use the [Admin SDK](/docs/backend#custom-auth). ## Auth Overview To get the current user in your application, you can use the `db.useUser` hook. ```tsx import db from '../lib/db'; function Dashboard() { const user = db.useUser(); return
Signed in as: {user.email}
; } ``` The `useUser` hook will throw an error if it is accessed while the user is not logged in, so it should be gated behind `` ```tsx import db from '../lib/db'; export default function App() { return (
Log in to see the dashboard!
); } function Dashboard() { // This component will only render if the user is signed in // so it's safe to call useUser here! const user = db.useUser(); return
Signed in as: {user.email}
; } ``` Use `` and `` to conditionally render components based on the user's authentication state. You can then use `db.auth.signOut()` to sign a user out. ```tsx import db from '../lib/db'; // ... Same app component from above function Dashboard() { const user = db.useUser(); return (
Signed in as: {user.email}
); } ``` Putting it all together, you can conditionally render a login and dashboard component like so: ```tsx import db from '../lib/db'; export default function App() { return (
); } function Dashboard() { // This component will only render if the user is signed in // so it's safe to call useUser here! const user = db.useUser(); return
Signed in as: {user.email}
; } function Login() { // Implement a login flow here via magic codes, OAuth, Clerk, etc. } ``` To implement a login flow use one of the authentication method guides below. ## Authentication Methods ## Additional Auth APIs Sometimes you need finer control over the state of auth in your application. In those cases, you can use some of the lower-level API. ### useAuth Use `useAuth` to fetch the current user. In this example we guard against loading our `Main` component until a user is logged in ```javascript function App() { const { isLoading, user, error } = db.useAuth(); if (isLoading) { return null; // or a loading spinner } if (error) { return
Uh oh! {error.message}
; } if (user) { return
; } return ; } ``` ### Get auth For scenarios where you want to know the current auth state without subscribing to changes, you can use `getAuth`. ```javascript const user = await db.getAuth(); console.log('logged in as', user.email); ``` # Magic Code Auth How to add magic code auth to your Instant app. Instant supports a "magic-code" flow for auth. Users provide their email, we send them a login code on your behalf, and they authenticate with your app. Choose the platform you're building for to see a full example. ## Full Magic Code Example Here's a full example of magic code auth in a React app. Open up your `app/page.tsx` file, and replace the entirety of it with the following code: ```tsx 'use client'; import React, { useState } from 'react'; import { init } from '@instantdb/react'; const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { return ( <>
); } function Main() { const user = db.useUser(); return (

Hello {user.email}!

); } function Login() { const [sentEmail, setSentEmail] = useState(''); return (
{!sentEmail ? ( ) : ( )}
); } function EmailStep({ onSendEmail }: { onSendEmail: (email: string) => void }) { const inputRef = React.useRef(null); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const inputEl = inputRef.current!; const email = inputEl.value; onSendEmail(email); db.auth.sendMagicCode({ email }).catch((err) => { alert('Uh oh :' + err.body?.message); onSendEmail(''); }); }; return (

Let's log you in

Enter your email, and we'll send you a verification code. We'll create an account for you too if you don't already have one.

); } function CodeStep({ sentEmail }: { sentEmail: string }) { const inputRef = React.useRef(null); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const inputEl = inputRef.current!; const code = inputEl.value; db.auth.signInWithMagicCode({ email: sentEmail, code }).catch((err) => { inputEl.value = ''; alert('Uh oh :' + err.body?.message); }); }; return (

Enter your code

We sent an email to {sentEmail}. Check your email, and paste the code you see.

); } export default App; ``` Here's a full example of magic code auth in a React Native app. Open up your `app/index.tsx` file, and replace the entirety of it with the following code: ```tsx import React, { useState } from 'react'; import { View, Text, TextInput, Button, Alert, StyleSheet } from 'react-native'; import { init } from '@instantdb/react-native'; const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { const { isLoading, user, error } = db.useAuth(); if (isLoading) { return Loading...; } if (error) { return Uh oh! {error.message}; } if (user) { return
; } return ; } function Main() { const user = db.useUser(); return ( Hello {user.email}! `; document.getElementById('sign-out')!.addEventListener('click', () => { db.auth.signOut(); }); } function renderLogin() { if (!sentEmail) { renderEmailStep(); } else { renderCodeStep(); } } function renderEmailStep() { app.innerHTML = `

Let's log you in

Enter your email, and we'll send you a verification code. We'll create an account for you too if you don't already have one.

`; document.getElementById('email-form')!.addEventListener('submit', (e) => { e.preventDefault(); const email = (document.getElementById('email-input') as HTMLInputElement) .value; sentEmail = email; renderLogin(); db.auth.sendMagicCode({ email }).catch((err) => { alert('Uh oh: ' + err.body?.message); sentEmail = ''; renderLogin(); }); }); } function renderCodeStep() { app.innerHTML = `

Enter your code

We sent an email to ${sentEmail}. Check your email, and paste the code you see.

`; document.getElementById('code-form')!.addEventListener('submit', (e) => { e.preventDefault(); const codeInput = document.getElementById('code-input') as HTMLInputElement; const code = codeInput.value; db.auth.signInWithMagicCode({ email: sentEmail, code }).catch((err) => { alert('Uh oh: ' + err.body?.message); codeInput.value = ''; }); }); } db.subscribeAuth((auth) => { renderApp(auth.user); }); ``` Make sure you have a `
` element in your HTML. --- **Let's dig deeper.** We created a login flow to handle magic code auth. Of note is `auth.sendMagicCode` and `auth.signInWithMagicCode`. On successful validation, Instant's backend will return a user object with a refresh token. The client SDK will then restart the websocket connection with Instant's sync layer and provide the refresh token. When doing queries or transactions, the refresh token will be used to hydrate `auth` on the backend during permission checks. On the client, auth will now be populated with a `user` -- huzzah! ## Send a Magic Code ```javascript db.auth.sendMagicCode({ email }).catch((err) => { alert('Uh oh :' + err.body?.message); onSendEmail(''); }); ``` Use `auth.sendMagicCode` to generate a magic code on instant's backend and email it to the user. ## Sign in with Magic Code ```javascript db.auth.signInWithMagicCode({ email: sentEmail, code }).catch((err) => { inputEl.value = ''; alert('Uh oh :' + err.body?.message); }); ``` You can then use `auth.signInWithMagicCode` to authenticate the user with the magic code they provided. # Guest Auth Let your users try your app before creating an account Instant supports guest authentication. This allows your users to try your app before signing up and ensures they can keep all their data when they decide to create a full account with their email. ## Signing in as a Guest Use `db.auth.signInAsGuest()` to create a new guest user. This will create a new guest user with an id, but no email address. ```tsx 'use client'; import React, { useState } from 'react'; import { init, User } from '@instantdb/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { return ( <>
); } function Main() { const user = db.useUser(); return (

Hello {user.isGuest ? 'Guest' : user.email}!

); } function Login() { return (
); } export default App; ``` ## Upgrading to a full user When a guest user is ready to create a permanent account, you can use any of Instant's sign-in methods. The guest user will be automatically upgraded to a full user. Here is a full example using magic code auth: ```tsx 'use client'; import React, { useState } from 'react'; import { init, User } from '@instantdb/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { return ( <>
); } function Main() { const user: User = db.useUser(); return (

Hello {user.isGuest ? 'Guest' : user.email}!

{user.isGuest && }
); } function Upgrade() { const [sentEmail, setSentEmail] = useState(''); return (
{!sentEmail ? ( ) : ( )}
); } function Login() { const [sentEmail, setSentEmail] = useState(''); return (
{!sentEmail ? ( ) : ( )}
); } function EmailStep({ onSendEmail }: { onSendEmail: (email: string) => void }) { const inputRef = React.useRef(null); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const inputEl = inputRef.current!; const email = inputEl.value; onSendEmail(email); db.auth.sendMagicCode({ email }).catch((err) => { alert('Uh oh :' + err.body?.message); onSendEmail(''); }); }; return (

Let's log you in

Enter your email, and we'll send you a verification code. We'll create an account for you too if you don't already have one.

); } function CodeStep({ sentEmail }: { sentEmail: string }) { const inputRef = React.useRef(null); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const inputEl = inputRef.current!; const code = inputEl.value; db.auth.signInWithMagicCode({ email: sentEmail, code }).catch((err) => { inputEl.value = ''; alert('Uh oh :' + err.body?.message); }); }; return (

Enter your code

We sent an email to {sentEmail}. Check your email, and paste the code you see.

); } export default App; ``` ## Handling conflicting users If a guest user signs up with an email that is not already associated with an existing account, their user `id` remains the same, and they retain access to all data they created as a guest. However, if a user with that email already exists, the guest user's data may need to be merged. You can fetch the list of guest users for a user with this query: ```ts const query = { $users: { $: { where: { linkedPrimaryUser: user.id, }, }, }, }; ``` The linked guest users are also available on the user itself: ```ts const query = { $users: { $: { where: { id: user.id, }, linkedGuestUsers: {}, }, }, }; ``` You can then query for the data owned by those guest users and transfer it to the primary account. The specific implementation will depend on your application's data model. To enable your user to access the data stored by the guest users, you can update your rules to grant access: ```diff { "todos": { "bind": [ "isOwner", "data.owner == auth.id", + "isGuestOwner", "data.owner in auth.ref('$user.linkedGuestUsers.id')" ], "allow": { - "view": "isOwner", + "view": "isOwner || isGuestOwner", "create": "isOwner", - "update": "isOwner", + "update": "isOwner || isGuestOwner", "delete": "isOwner" } } } ``` Here's an example of how you might transfer `todos` from a guest account to the primary user: ```ts function App() { const user = db.useUser(); useEffect(() => { if (user.isGuest) return; const transferGuestData = async () => { // Get the linked guest user const { data: { $users }, } = await db.queryOnce({ $users: { $: { where: { linkedPrimaryUser: user.id }, limit: 1, order: { serverCreatedAt: desc }, }, }, }); const guestId = $users[0]?.id; if (!guestId) return; // Get the data for the guest user const { data: { todos }, } = await db.queryOnce({ todos: { $: { where: { owner: guestId }, }, }, }); if (!todos.length) return; // Update owner on all of the guest's todo entities const txes = todos.map((todo) => db.tx.todos[todo.id].update({ owner: user.id }), ); await db.transact(txes); }; transferGuestData(); }, [user]); } ``` # Google OAuth How to add Google OAuth to your Instant app. Instant supports logging users in with their Google account. There are a few ways to do this: it depends on whether you are building for web or React Native. Choose the option that sounds best to you, and the rest of the document will show you how to add Sign in with Google to your app. **Building for Web?** **Building for React Native?** ## Overview There are three main steps: 1. **Google Console**: Set up your consent screen and create an Oauth client. 2. **Instant Dashboard**: Connect your Oauth client to Instant 3. **Your app**: Add some code to log in with Google! Let's dive deeper in each step: ## 1. Set up your consent screen and create an Oauth client Head on over to . You should be in the "Credentials" section. **Configure your Google OAuth consent screen** - Click "CONFIGURE CONSENT SCREEN." If you already have a consent screen, you can skip to the next step. - Select "External" and click "CREATE". - Add your app's name, a support email, and developer contact information. Click "Save and continue". - No need to add scopes or test users. Click "Save and continue" for the next screens. Until you reach the "Summary" screen, click "Back to dashboard". **Create an OAuth client for Google** - From Google Console, click "+ CREATE CREDENTIALS" - Select "OAuth client ID" - Select "Web application" as the application type. - Add `https://api.instantdb.com/runtime/oauth/callback` as an Authorized redirect URI. - If you're testing from localhost, **add both `http://localhost`** and `http://localhost:3000` to "Authorized JavaScript origins", replacing `3000` with the port you use. - For production, add your website's domain. And with that you have your Oauth client! Save your Client ID and your Client Secret -- you'll need it for the next step! For native auth, each platform needs an Oauth Client. If you support both iOS or Android for example, you'll create two clients. Here are the steps: - From Google Console, click "+ CREATE CREDENTIALS" - Select "OAuth client ID" - Select "iOS" or "Android" as the application type. - Fill in your bundle information. And with that you're ready! Save your Client IDs -- you'll need it for the next step! ## 2. Connect your Oauth client to Instant Go to the and select the `Auth` tab for your app. **Add your Oauth Client on Instant** - Click "Set up Google" - Enter your "Client ID" - Enter your "Client Secret" - Check "I added the redirect to Google" (make sure you actually did this!) - Click "Add Client" And voila, you are connected! **Register your website with Instant** In the `Auth` tab, add the url of the websites where you are using Instant to the Redirect Origins. If you're testing from localhost, add `http://localhost:3000`, replacing `3000` with the port you use. For production, add your website's domain. Go to the and select the `Auth` tab for your app. For each Oauth Client you created, add it to Instant: - Click "Set up Google" - Enter your "Client ID" - Make sure "skip nonce checks" is enabled. - Click "Add Client" And voila, you are connected! ## 3. Add some code! **Method: Google Sign in Button for Web** We'll use . The benefit of using Google's button is that you can display your app's name in the consent screen. There are two steps to the code: 1. Use the Sign in Button to auth with Google and get an `idToken` 2. Pass the token on to `db.auth.signInWithIdToken`, and you are logged in! Let's do that. **Using React** If you're using React, the easiest way to include the Sign in Button is through the package: ```shell npm install @react-oauth/google ``` Once you install it, include the button, and use `db.auth.signInWithIdToken` to complete sign in. Here's a full example: ```jsx 'use client'; import React, { useState } from 'react'; import { init } from '@instantdb/react'; import { GoogleOAuthProvider, GoogleLogin } from '@react-oauth/google'; const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); // e.g. 89602129-cuf0j.apps.googleusercontent.com const GOOGLE_CLIENT_ID = 'REPLACE_ME'; // Use the google client name in the Instant dashboard auth tab const GOOGLE_CLIENT_NAME = 'REPLACE_ME'; function App() { return ( <> ); } function UserInfo() { const user = db.useUser(); return

Hello {user.email}!

; } function Login() { const [nonce] = useState(crypto.randomUUID()); return ( alert('Login failed')} onSuccess={({ credential }) => { db.auth .signInWithIdToken({ clientName: GOOGLE_CLIENT_NAME, idToken: credential, // Make sure this is the same nonce you passed as a prop // to the GoogleLogin button nonce, }) .catch((err) => { alert('Uh oh: ' + err.body?.message); }); }} /> ); } ``` **Not using React?** If you're not using React or prefer to embed the button yourself, refer to on how to create the button and load their client library When creating your button, make sure to set the `data-ux_mode="popup"`. Your `data-callback` function should look like: ```jsx async function handleSignInWithGoogle(response) { await db.auth.signInWithIdToken({ // Use the google client name in the Instant dashboard auth tab clientName: 'REPLACE_ME', idToken: response.credential, // make sure this is the same nonce you set in data-nonce nonce: 'REPLACE_ME', }); } ``` **Method: Web Redirect** If you don't want to use the google styled buttons, you can use the redirect flow instead. Create an authorization URL via `db.auth.createAuthorizationURL` and then use the url to create a link. Here's a full example: ```jsx 'use client'; import React, { useState } from 'react'; import { init } from '@instantdb/react'; const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); const url = db.auth.createAuthorizationURL({ // Use the google client name in the Instant dashboard auth tab clientName: 'REPLACE_ME', redirectURL: window.location.href, }); import React from 'react'; export default function App() { return ( <> ); } function UserInfo() { const user = db.useUser(); return

Hello {user.email}!

; } function Login() { return Log in with Google; } ``` When your users clicks on the link, they'll be redirected to Google to start the OAuth flow and then back to your site. Instant will automatically log them in to your app when they are redirected! **Method: Expo Web Auth** Instant comes with support for Expo's AuthSession library. To use it, you need to: 1. Set up AuthSession 2. Register your app with Instant 3. Use AuthSession to log in with Google! Let's do that. **Set up AuthSession** If you haven't already, follow the AuthSession from the Expo docs. Next, add the following dependencies: ```shell npx expo install expo-auth-session expo-crypto ``` Update your app.json with your scheme: ```json { "expo": { "scheme": "mycoolredirect" } } ``` **Register your app with Instant** Now that you have you App Scheme, it's time to tell Instant about it. From the tab on the Instant dashboard, add a redirect origin of type "App scheme". For development with expo add `exp://` and your scheme, e.g. `mycoolredirect://`. **Use AuthSession to log in with Google!** And from here you're ready to add a login button to your expo app! Here's a full example ```jsx import { View, Text, Button, StyleSheet } from 'react-native'; import { init } from '@instantdb/react-native'; import { makeRedirectUri, useAuthRequest, useAutoDiscovery, } from 'expo-auth-session'; const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { return ( <> ); } function UserInfo() { const user = db.useUser(); return Hello {user.email}!; } function Login() { const discovery = useAutoDiscovery(db.auth.issuerURI()); const [request, _response, promptAsync] = useAuthRequest( { // The unique name you gave the OAuth client when you // registered it on the Instant dashboard clientId: 'YOUR_INSTANT_AUTH_CLIENT_NAME', redirectUri: makeRedirectUri(), }, discovery, ); return ( ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, }); export default App; ``` **Method: Native Auth** You can use [react-native-google-signin/google-signin](https://github.com/react-native-google-signin/google-signin), to authenticate natively on Google. There are three steps: 1. Set up google-signin on Expo 1. Use the Sign in Button to auth with Google and get an `idToken` 1. Pass the token on to `db.auth.signInWithIdToken`, and you are logged in! Let's do that. **Set up google-signin on Expo** First, let's install the package: ``` npx expo install @react-native-google-signin/google-signin ``` Then, follow the google-signin to set it up with Expo. **Use google-signin to log in with Google!** Now you're ready to add the Google Signin button to your expo app! Here's a full example: ```jsx import { View, Text, Button, StyleSheet } from 'react-native'; import { init } from '@instantdb/react-native'; import { GoogleSignin, GoogleSigninButton, } from '@react-native-google-signin/google-signin'; const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); GoogleSignin.configure({ // See https://react-native-google-signin.github.io/docs/original#configure iosClientId: 'YOUR_IOS_CLIENT_ID', }); function App() { return ( <> Loading...}> ); } function UserInfo() { const user = db.useUser(); return Hello {user.email}!; } function Login() { return ( { // 1. Sign in to Google await GoogleSignin.hasPlayServices(); const userInfo = await GoogleSignin.signIn(); const idToken = userInfo.data?.idToken; if (!idToken) { console.error('no ID token present!'); return; } // 2. Use your token, and sign into InstantDB! try { const res = await db.auth.signInWithIdToken({ // The unique name you gave the OAuth client when you // registered it on the Instant dashboard clientName: 'YOUR_INSTANT_AUTH_CLIENT_NAME', idToken, }); console.log('logged in!', res); } catch (error) { console.log('error signing in', error); } }} /> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, }); export default App; ``` # Sign In with Apple How to add Sign In with Apple to your Instant app. Instant supports Sign In with Apple on the Web and in native applications. ## Step 1: Create App ID - Navigate to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list) - Select _Identifiers_ - Click _+_ - _Register a new identifier_ → Select _App IDs_ - _Select a type_ → Select _App_ - _Capabilities_ → _Sign In with Apple_ → Check - Fill in _Bundle ID_ and _Description_ - Click _Register_ ## Step 2: Create Services ID - Navigate to [Services IDs](https://developer.apple.com/account/resources/identifiers/list/serviceId) - Click _+_ - _Register a new identifier_ → Select _Services IDs_ - Fill in _Description_ and _Identifier_. You’ll need this _Identifier_ later - Click _Register_ ## Step 3: Configure Services ID (Web Popup flow) - Select newly created Services ID - Enable _Sign In with Apple_ - Click _Configure_ - Select _Primary App ID_ from Step 1 - To _Domains_, add your app domain (e.g. `myapp.com`) - To _Return URLs_, add URL of your app where authentication happens (e.g. `https://myapp.com/signin`) - Click _Continue_ → _Save_ ## Step 3: Configure Services ID (Web Redirect flow) - Select newly created Services ID - Enable _Sign In with Apple_ - Click _Configure_ - Select _Primary App ID_ from Step 1 - To _Domains_, add `api.instantdb.com` - To _Return URLs_, add `https://api.instantdb.com/runtime/oauth/callback` - Click _Continue_ → _Save_ ## Step 3.5: Generate Private Key (Web Redirect flow only) - Navigate to [Keys](https://developer.apple.com/account/resources/authkeys/list) - Click _+_ - Fill in _Name_ and _Description_ - Check _Sign in with Apple_ - Configure → select _App ID_ from Step 1 - _Continue_ → _Register_ - Download key file ## Step 3: Configure Services ID (React Native flow) This step is not needed for Expo. ## Step 4: Register your OAuth client with Instant - Go to the Instant dashboard and select _Auth_ tab. - Select _Add Apple Client_ - Select unique _clientName_ (`apple` by default, will be used in `db.auth` calls) - Fill in _Services ID_ from Step 2 - Fill in _Team ID_ from [Membership details](https://developer.apple.com/account#MembershipDetailsCard) - Fill in _Key ID_ from Step 3.5 - Fill in _Private Key_ by copying file content from Step 3.5 - Click `Add Apple Client` - Go to the Instant dashboard and select _Auth_ tab. - Select _Add Apple Client_ - Select unique _clientName_ (`apple` by default, will be used in `db.auth` calls) - Fill in _Services ID_ from Step 2 - Click `Add Apple Client` ## Step 4.5: Whitelist your domain in Instant (Web Redirect flow only) - In Instant Dashboard, Click _Redirect Origins_ → _Add an origin_ - Add your app’s domain (e.g. `myapp.com`) ## Step 5: Add Sign In code to your app (Web Popup flow) Add Apple Sign In library to your app: ``` https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js ``` Initialize with `Services ID` from Step 2: ```jsx AppleID.auth.init({ clientId: '', scope: 'name email', redirectURI: window.location.href, }); ``` Implement `signInPopup` using `clientName` from Step 4: ```jsx async function signInPopup() { let nonce = crypto.randomUUID(); // authenticate with Apple let resp = await AppleID.auth.signIn({ nonce: nonce, usePopup: true, }); // authenticate with Instant await db.auth.signInWithIdToken({ clientName: '', idToken: resp.authorization.id_token, nonce: nonce, }); } ``` Add Sign In button: ```jsx ``` ## Step 5: Add Sign In code to your app (Web Popup flow) Create Sign In link using `clientName` from Step 4: ``` const authUrl = db.auth.createAuthorizationURL({ clientName: '', redirectURL: window.location.href, }); ``` Add a link uses `authUrl`: ``` Sign In with Apple ``` That’s it! ## Step 5: Add Sign In code to your app (React Native flow) Instant comes with support for [Expo AppleAuthentication library](https://docs.expo.dev/versions/latest/sdk/apple-authentication/). Add dependency: ```shell npx expo install expo-apple-authentication ``` Update `app.json` by adding: ```json { "expo": { "ios": { "usesAppleSignIn": true } } } ``` Go to Instant dashboard → Auth tab → Redirect Origins → Add an origin. Add `exp://` for development with Expo. Authenticate with Apple and then pass identityToken to Instant along with `clientName` from Step 4: ```jsx const [nonce] = useState('' + Math.random()); try { // sign in with Apple const credential = await AppleAuthentication.signInAsync({ requestedScopes: [ AppleAuthentication.AppleAuthenticationScope.FULL_NAME, AppleAuthentication.AppleAuthenticationScope.EMAIL, ], nonce: nonce, }); // pass identityToken to Instant db.auth .signInWithIdToken({ clientName: '', idToken: credential.identityToken, nonce: nonce, }) .catch((err) => { console.log('Error', err.body?.message, err); }); } catch (e) { if (e.code === 'ERR_REQUEST_CANCELED') { // handle that the user canceled the sign-in flow } else { // handle other errors } } ``` Sign out code: ```jsx ); } function Login() { const discovery = useAutoDiscovery(db.auth.issuerURI()); const [request, _response, promptAsync] = useAuthRequest( { // The unique name you gave the OAuth client when you // registered it on the Instant dashboard clientId: 'github-web', redirectUri: makeRedirectUri(), }, discovery, ); return ( ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, }); export default App; ``` # Clerk How to integrate Clerk's auth flow with Instant. Instant supports auth with Clerk. ## Setup **Step 1: Configure Clerk** Go to your Clerk dashboard, navigate to [`Sessions`](https://dashboard.clerk.com/last-active?path=sessions), then click the `Edit` button in the `Customize session token` section. Add the email and email_verified claims to your session token: ```json { "email": "{{user.primary_email_address}}", "email_verified": "{{user.email_verified}}" } ``` You can have additional claims as long as the `email` claim is set to `{{user.primary_email_address}}` and the `email_verified` claim is set to `{{user.email_verified}}`. ![Clerk token form](/img/docs/clerk-token-form.png) **Step 2: Get your Clerk Publishable key** On the Clerk dashboard, navigate to [`API keys`](https://dashboard.clerk.com/last-active?path=api-keys), then copy the `Publishable key`. It should start with `pk_`. **Step 3: Register your Clerk Publishable key with your instant app** Go to the Instant dashboard, navigate to the `Auth` tab and add a new clerk app with the publishable key you copied. ## Usage Use Clerk's `getToken` helper to get a session JWT for your signed-in user. Then call Instant's `db.auth.signInWithIdToken` with the JWT and the client name you set on the Instant dashboard. When you call `db.auth.signInWithIdToken`, Instant will verify that the JWT was signed by your Clerk app. If verified, Instant will use the email in the JWT's claims to lookup your user or create a new one and create a long-lived session. Be sure to call Instant's `db.auth.signOut` when you want to sign the user out. Here is a full example using clerk's next.js library: ```javascript 'use client'; import { useAuth, ClerkProvider, SignInButton, SignedIn, SignedOut, } from '@clerk/nextjs'; import { init } from '@instantdb/react'; import { useEffect } from 'react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); // Use the clerk client name you set in the Instant dashboard auth tab const CLERK_CLIENT_NAME = 'REPLACE_ME'; function ClerkSignedInComponent() { const { getToken, signOut } = useAuth(); const signInToInstantWithClerkToken = async () => { // getToken gets the jwt from Clerk for your signed in user. const idToken = await getToken(); if (!idToken) { // No jwt, can't sign in to instant return; } // Create a long-lived session with Instant for your clerk user // It will look up the user by email or create a new user with // the email address in the session token. db.auth.signInWithIdToken({ clientName: CLERK_CLIENT_NAME, idToken: idToken, }); }; useEffect(() => { signInToInstantWithClerkToken(); }, []); const { isLoading, user, error } = db.useAuth(); if (isLoading) { return
Loading...
; } if (error) { return
Error signing in to Instant! {error.message}
; } if (user) { return (

Signed in with Instant through Clerk!

{' '}
); } return (
); } function App() { return ( ); } export default App; ``` # Firebase Auth How to integrate Firebase's auth flow with Instant. Instant supports delegating auth to Firebase Auth. ## Setup **Step 1: Get your Firebase Project ID** On the [Firebase dashboard](https://console.firebase.google.com/), open your project and navigate to navigate to `Project Overview` > `⚙` > `Project Settings`, then copy the `Project ID`. **Step 2: Register your Firebase Project ID with your instant app** Go to the Instant dashboard, navigate to the `Auth` tab and add a new firebase auth app with the Project ID you copied. ## Usage Use Firebase's `getIdToken` helper to get a JWT for your signed-in user. Then call Instant's `db.auth.signInWithIdToken` with the JWT and the client name you set on the Instant dashboard. When you call `db.auth.signInWithIdToken`, Instant will verify that the JWT was signed by your Firebase app. If verified, Instant use the email in the JWT's claims to lookup your user or create a new one and create a long-lived session. Be sure to call Instant's `db.auth.signOut` when you want to sign the user out. Here is a full example: ```tsx 'use client'; import { init } from '@instantdb/react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword, onAuthStateChanged, } from 'firebase/auth'; import { useEffect, useState } from 'react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); // Use the firebase client name you set in the Instant dashboard auth tab const FIREBASE_CLIENT_NAME = 'REPLACE_ME'; const firebaseConfig = { // Use the same Project ID you set in the Instant dashboard auth tab projectId: 'REPLACE_ME', apiKey: 'REPLACE_ME', }; const firebaseApp = initializeApp(firebaseConfig); const firebaseAuth = getAuth(firebaseApp); function App() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); useEffect(() => { const unsubscribe = onAuthStateChanged(firebaseAuth, (user) => { if (user) { user.getIdToken().then((idToken) => { db.auth.signInWithIdToken({ idToken, clientName: FIREBASE_CLIENT_NAME, }); }); } else { db.auth.signOut(); } }); return () => unsubscribe(); }, []); const handleSignIn = async (e) => { e.preventDefault(); try { await signInWithEmailAndPassword(firebaseAuth, email, password); } catch (error) { console.error('Sign in error:', error); } }; const handleSignUp = async (e) => { e.preventDefault(); try { await createUserWithEmailAndPassword(firebaseAuth, email, password); } catch (error) { console.error('Sign up error:', error); } }; return ( <>
setEmail(e.target.value)} /> setPassword(e.target.value)} />
); } function SignedInComponent() { const user = db.useUser(); const handleSignOut = async () => { await firebaseAuth.signOut(); }; return (
Signed in as {user.email}!
); } export default App; ``` # Permissions How to secure your data with Instant's Rule Language. To secure user data, you can use Instant's Rule Language. Our rule language takes inspiration from Rails' ActiveRecord, Google's CEL, and JSON. Here's an example ruleset below. ```tsx // instant.perms.ts import type { InstantRules } from '@instantdb/react'; const rules = { "todos": { "allow": { "view": "auth.id != null", "create": "isOwner", "update": "isOwner && isStillOwner", "delete": "isOwner", }, "bind": { "isOwner": "auth.id != null && auth.id == data.creatorId", "isStillOwner": "auth.id != null && auth.id == newData.creatorId" } } } satisfies InstantRules; export default rules; ``` You can manage permissions via configuration files or through the Instant dashboard. ## Permissions as code With Instant you can define your permissions in code. If you haven't already, use the [CLI](/docs/cli) to generate an `instant.perms.ts` file: ```shell npx instant-cli@latest init ``` The CLI will guide you through picking an Instant app and generate these files for you. Once you've made changes to `instant.perms.ts`, you can use the CLI to push those changes to production: ```shell npx instant-cli@latest push perms ``` ## Permissions in the dashboard For each app in your dashboard, you’ll see a permissions editor. Permissions are expressed as JSON. Each top level key represents one of your namespaces — for example `goals`, `todos`, and the like. There is also a special top-level key `attrs` for defining permissions on creating new types of namespaces and attributes. ## Namespaces For each namespace you can define `allow` rules for `view`, `create`, `update`, `delete`, as well as special rules for fields. Rules must be boolean expressions. If a rule is not set then by default it evaluates to true. The following three rulesets are all equivalent: In this example we explicitly set each action for `todos` to true ```json { "todos": { "allow": { "view": "true", "create": "true", "update": "true", "delete": "true" } } } ``` In this example we explicitly set `view` to be true. However, all the remaining actions for `todo` also default to true. ```json { "todos": { "allow": { "view": "true" } } } ``` In this example we set no rules, and thus all permission checks pass. ```json {} ``` When you start developing you probably won't worry about permissions. However, once you start shipping your app to users you will want to secure their data! ### View `view` rules are evaluated when doing `db.useQuery`. On the backend every object that satisfies a query will run through the `view` rule before being passed back to the client. This means as a developer you can ensure that no matter what query a user executes, they’ll _only_ see data that they are allowed to see. ### Create, Update, Delete Similarly, for each object in a transaction, we make sure to evaluate the respective `create`, `update`, and `delete` rule. Transactions will fail if a user does not have adequate permission. ### Fields You can also define field-level permissions. For example, you may want to make the `$users` table public, but hide email addresses. Here's how you could do that: ```json { "$users": { "allow": { "view": "true" }, "fields": { "email": "auth.id == data.id" } } } ``` ### Default permissions By default, all permissions are considered to be `"true"`. To change that, use `"$default"` key. This: ```json { "todos": { "allow": { "$default": "false" } } } ``` is equivalent to this: ```json { "todos": { "allow": { "view": "false", "create": "false", "update": "false", "delete": "false" } } } ``` Specific keys can override defaults: ```json { "todos": { "allow": { "$default": "false", "view": "true" } } } ``` You can use `$default` as the namespace: ```json { "$default": { "allow": { "view": "false" } }, "todos": { "allow": { "view": "true" } } } ``` Finally, the ultimate default: ```json { "$default": { "allow": { "$default": "false" } } } ``` ## Attrs Attrs are a special kind of namespace for creating new types of data on the fly. Currently we only support create rules on attrs. During development you likely don't need to lock this rule down, but once you ship you will likely want to set this permission to `false` Suppose our data model looks like this ```json { "goals": { "id": UUID, "title": string } } ``` And we have a rules defined as ```json { "attrs": { "allow": { "create": "false" } } } ``` Then we could create goals with existing attr types: ```javascript db.transact(db.tx.goals[id()].update({title: "Hello World"}) ``` But we would not be able to create goals with new attr types: ```javascript db.transact(db.tx.goals[id()].update({title: "Hello World", priority: "high"}) ``` ## CEL expressions Inside each rule, you can write CEL code that evaluates to either `true` or `false`. ```json { "todos": { "allow": { "view": "auth.id != null", "create": "auth.id in data.ref('creator.id')", "update": "!(newData.title == data.title)", "delete": "'joe@instantdb.com' in data.ref('users.email')" } } } ``` The above example shows a taste of the kind of rules you can write :) ### data `data` refers to the object you have saved. This will be populated when used for `view`, `create`, `update`, `delete` rules. ### newData In `update`, you'll also have access to `newData`. This refers to the changes that are being made to the object. ### bind `bind` allows you to alias logic. The following are equivalent ```json { "todos": { "allow": { "create": "isOwner" }, "bind": { "isOwner": "auth.id != null && auth.id == data.creatorId" } } } ``` ```json { "todos": { "allow": { "create": "auth.id != null && auth.id == data.creatorId" } } } ``` `bind` is useful for not repeating yourself and tidying up rules ```json { "todos": { "allow": { "create": "isOwner || isAdmin" }, "bind": { "isOwner": "auth.id != null && auth.id == data.creatorId", "isAdmin": "auth.email in ['joe@instantdb.com', 'stopa@instantdb.com']" } } } ``` ### ref You can also refer to relations in your permission checks. This rule restricts delete to only succeed on todos associated with a specific user email. ```json { "todos": { "allow": { "delete": "'joe@instantdb.com' in data.ref('users.email')" } } } ``` `ref` works on the `auth` object too. Here's how you could restrict `deletes` to users with the 'admin' role: ```json { "todos": { "allow": { "delete": "'admin' in auth.ref('$user.role.type')" }, }, }; ``` See [managing users](/docs/users) to learn more about that. ### ruleParams Imagine you have a `documents` namespace, and want to implement a rule like _"Only people who know my document's id can access it."_ You can use `ruleParams` to write that rule. `ruleParams` let you pass extra options to your queries and transactions. For example, pass a `knownDocId` param to our query: ```javascript // You could get your doc's id from the URL for example const myDocId = getId(window.location); const query = { docs: {}, }; const { data } = db.useQuery(query, { ruleParams: { knownDocId: myDocId }, // Pass the id to ruleParams! }); ``` Or to your transactions: ```js db.transact( db.tx.docs[id].ruleParams({ knownDocId: id }).update({ title: 'eat' }), ); ``` And then use it in your permission rules: ```json { "documents": { "allow": { "view": "data.id == ruleParams.knownDocId", "update": "data.id == ruleParams.knownDocId", "delete": "data.id == ruleParams.knownDocId" } } } ``` With that, you've implemented the rule _"Only people who know my document's id can access it."_! ### request.time The timestamp when the rules were run. You might use this in an update or create rule to ensure that a time is valid: ```json { "documents": { "allow": { "update": "math.abs((request.time - timestamp(data.updatedAt)).getMinutes()) <= 1" } } } ``` ### request.origin The origin that made the request, as a string, e.g. `https://example.com`. This may be null if the request was not made by a browser. ### request.ip The IP address of the originating request as a string, e.g. `192.251.68.254` or `2a03:2880:2110:df07:face:b00c::1`. ### request.modifiedFields In `create` and `update` rules, you have access to `request.modifiedFields` — a list of field names being set in the transaction. This is useful when you want to control which fields can be set by whom. #### Restricting fields on create You can use `request.modifiedFields` to prevent certain fields from being set on creation. For example, only admins should be able to set the `featured` field: ```json { "posts": { "allow": { "create": "isAdmin || !('featured' in request.modifiedFields)" }, "bind": { "isAdmin": "'admin' in auth.ref('$user.role.type')" } } } ``` This allows anyone to create posts, but only admins can set the `featured` field. #### Restricting fields on update You might want only the owner to edit a post's title and content, but allow anyone to increment a `likes` counter: ```json { "posts": { "allow": { "update": "isOwner || onlyModifiesLikes" }, "bind": { "isOwner": "auth.id == data.ownerId", "onlyModifiesLikes": "request.modifiedFields.all(field, field in ['likes'])" } } } ``` This rule allows the owner to update any field, but non-owners can only update the `likes` field. Since a transaction can update multiple fields at once, `request.modifiedFields` is a list. We use `.all(field, field in ['likes'])` to check that _every_ field being modified is in our allowed list. This prevents someone from sneaking in an unauthorized field change alongside an allowed one. You can allow multiple public fields: ```json { "posts": { "allow": { "update": "isOwner || onlyModifiesPublicFields" }, "bind": { "isOwner": "auth.id == data.ownerId", "onlyModifiesPublicFields": "request.modifiedFields.all(field, field in ['likes', 'viewCount'])" } } } ``` You can combine `modifiedFields` checks with other conditions. For example, require users to be logged in before updating public fields: ```json { "posts": { "allow": { "update": "isOwner || (isLoggedIn && onlyModifiesLikes)" }, "bind": { "isOwner": "auth.id == data.ownerId", "isLoggedIn": "auth.id != null", "onlyModifiesLikes": "request.modifiedFields.all(field, field in ['likes'])" } } } ``` You can also add value validation that only runs when a specific field is being modified. Use `'fieldName' in request.modifiedFields` to check if a field is being changed: ```json { "posts": { "allow": { "update": "isOwner || (onlyModifiesPublicFields && likesValid && titleValid)" }, "bind": { "isOwner": "auth.id == data.ownerId", "onlyModifiesPublicFields": "request.modifiedFields.all(field, field in ['likes', 'title'])", "likesValid": "!('likes' in request.modifiedFields) || newData.likes >= 0", "titleValid": "!('title' in request.modifiedFields) || size(newData.title) <= 100" } } } ``` This rule allows non-owners to update `likes` and `title`, but only if `likes` is non-negative and `title` is at most 100 characters. The `!('field' in request.modifiedFields) || condition` pattern means "either this field isn't being modified, or if it is, it must satisfy the condition." **Here are some more patterns** If you want to: access a document and _all related comments_ by one `knownDocId`: ```json { "docs": { "view": "data.id == ruleParams.knownDocId" }, "comment": { "view": "ruleParams.knownDocId in data.ref('parent.id')" } } ``` Or, if you want to allow multiple documents: ```js db.useQuery(..., { knownDocIds: [id1, id2, ...] }) ``` ```json { "docs": { "view": "data.id in ruleParams.knownDocIds" } } ``` To create a “share links” feature, where you have multiple links to the same doc, you can create a separate namespace: ```json { "docs": { "view": "ruleParams.secret in data.ref('docLinks.secret')" } } ``` Or if you want to separate “view links” from “edit links”, you can use two namespaces like this: ```json { "docs": { "view": "hasViewerSecret || hasEditorSecret", "update": "hasEditorSecret", "delete": "hasEditorSecret", "bind": { "hasViewerSecret": "ruleParams.secret in data.ref('docViewLinks.secret')", "hasEditorSecret": "ruleParams.secret in data.ref('docEditLinks.secret')" } } } ``` # Managing users How to manage users in your Instant app. ## See users in your app You can manage users in your app using the `$users` namespace. This namespace is automatically created when you create an app. You'll see the `$users` namespace in the `Explorer` tab with all the users in your app! ## Querying users The `$users` namespace can be queried like any normal namespace. However, we've set some default permissions so that only a logged-in user can view their own data. ```javascript // instant.perms.ts import type { InstantRules } from "@instantdb/react"; const rules = { $users: { allow: { view: 'auth.id == data.id', create: 'false', delete: 'false', update: 'false', }, }, } satisfies InstantRules; export default rules; ``` Since `$users` is a managed namespace, you can override `view` and `update` rules, but not `create` or `delete`. These are handled by the Instant backend. ## Sharing user data If you want to make the users table public, you can always change the `view` permission. If you do this, be sure to write an appropriate `field` permission on `emails` so that those columns don't leak. ```javascript // instant.perms.ts import type { InstantRules } from "@instantdb/react"; const rules = { $users: { allow: { view: 'true', // anyone can see users create: 'false', delete: 'false', update: 'false', }, fields: { email: "auth.id == data.id" // but only the logged in user can see their own email. } }, } satisfies InstantRules; export default rules; ``` ## Adding properties You can add optional properties on the `$users` table. Here is an example of a schema for a todo app where users have nicknames and roles: ```javascript // instant.schema.ts import { i } from '@instantdb/react'; const _schema = i.schema({ entities: { $users: i.entity({ email: i.any().unique().indexed(), nickname: i.string().optional(), // Users now have a `nickname` property }), roles: i.entity({ type: i.string().indexed(), }), todos: i.entity({ text: i.string(), completed: i.boolean(), }), }, links: { userRoles: { forward: { on: '$users', has: 'many', label: 'roles' }, reverse: { on: 'roles', has: 'many', label: 'users' }, }, todoOwner: { forward: { on: 'todos', has: 'one', label: 'owner' }, reverse: { on: '$users', has: 'many', label: 'todos'}, }, }, }); // This helps TypeScript display nicer intellisense type _AppSchema = typeof _schema; interface AppSchema extends _AppSchema {} const schema: AppSchema = _schema; export type { AppSchema }; export default schema; ``` ### Links We created two links `userRoles`, `todoOwner`: ```typescript // instant.schema.ts import { i } from '@instantdb/react'; const _schema = i.schema({ // .. links: { userRoles: { forward: { on: '$users', has: 'many', label: 'roles' }, reverse: { on: 'roles', has: 'many', label: 'users' }, }, todoOwner: { forward: { on: 'todos', has: 'one', label: 'owner' }, reverse: { on: '$users', has: 'many', label: 'todos' }, }, }, }); ``` Notice that none of the links are required. You can't require links for `$users`. ### Attributes Now look at the `nickname` attribute we just added: ```typescript // instant.schema.ts import { i } from '@instantdb/react'; const _schema = i.schema({ entities: { $users: i.entity({ email: i.any().unique().indexed(), nickname: i.string().optional(), // Users now have a `nickname` property }), }, // ... }); ``` Note that `nickname` is optional too. Custom columns on `$users` have to be optional. --- Once done, you can include user information in the client like so: ```javascript // Creates a todo and links the current user as an owner const addTodo = (newTodo, currentUser) => { const newId = id(); db.transact( db.tx.todos[newId] .update({ text: newTodo, completed: false }) // Link the todo to the user with the `owner` label we defined in the schema .link({ owner: currentUser.id }), ); }; // Creates or updates a user profile with a nickname and links it to the // current user const updateNick = (newNick, currentUser) => { db.transact([db.tx.$users[currentUser.id].update({ nickname: newNick })]); }; ``` At the moment you can only use `transact` to update the custom properties you added. Changing default columns like `email` would cause the transaction to fail. ## User permissions You can reference the `$users` namespace in your permission rules just like a normal namespace. For example, you can restrict a user to only update their own todos like so: ```javascript export default { // users perms... todos: { allow: { // owner is the label from the todos namespace to the $users namespace update: "auth.id in data.ref('owner.id')", }, }, }; ``` You can also traverse the `$users` namespace directly from the `auth` object via `auth.ref`. When using `auth.ref` the arg must start with `$user`. Here's the equivalent rule to the one above using `auth.ref`: ```javascript export default { // users perms... todos: { allow: { // We traverse the users links directly from the auth object update: "data.id in auth.ref('$user.todos.id')", }, }, }; ``` By creating links to `$users` and leveraging `auth.ref`, you can expressively build more complex permission rules. ```javascript export default { // users perms... todos: { bind: { isAdmin: "'admin' in auth.ref('$user.roles.type')", isOwner: "data.id in auth.ref('$user.todos.id')", }, allow: { // We traverse the users links directly from the auth object update: 'isAdmin || isOwner', }, }, }; ``` # Presence, Cursors, and Activity How to add ephemeral features like presence and cursors to your Instant app. Sometimes you want to show real-time updates to users without persisting the data to your database. Common scenarios include: - Shared cursors in a collaborative whiteboard like Figma - Who's online in a document editor like Google Docs - Typing indicators in chat apps like Discord - Live reactions in a video streaming app like Twitch Instant provides three primitives for quickly building these ephemeral experiences: rooms, presence, and topics. **Rooms** A room represents a temporary context for realtime events. Users in the same room will receive updates from every other user in that room. **Presence** Presence is an object that each peer shares with every other peer. When a user updates their presence, it's instantly replicated to all users in that room. Presence persists throughout the remainder of a user's connection, and is automatically cleaned up when a user leaves the room. You can use presence to build features like "who's online." Instant's cursor and typing indicator are both built on top of the presence API. **Topics** Topics have "fire and forget" semantics, and are better suited for data that doesn't need any sort of persistence. When a user publishes a topic, a callback is fired for every other user in the room listening for that topic. You can use topics to build features like "live reactions." The real-time emoji button panel on Instant's homepage is built using the topics API. **Transact vs. Ephemeral** You may be thinking when would I use `transact` vs `presence` vs `topics`? Here's a simple breakdown: - Use `transact` when you need to persist data to the db. For example, when a user sends a message in a chat app. - Use `presence` when you need to persist data in a room but not to the db. For example, showing who's currently viewing a document. - Use `topics` when you need to broadcast data to a room, but don't need to persist it. For example, sending a live reaction to a video stream. ## Setup To obtain a room reference, call `db.room(roomType, roomId)` ```typescript import { init } from '@instantdb/react'; // Instant app const APP_ID = '__APP_ID__'; // db will export all the presence hooks you need! const db = init({ appId: APP_ID }); // Specifying a room type and room id gives you the power to // restrict sharing to a specific room. However you can also just use // `db.room()` to share presence and topics to an Instant generated default room const roomId = 'hacker-chat-room-id'; const room = db.room('chat', roomId); ``` ## Typesafety By default rooms accept any kind of data. However, you can enforce typesafety with a schema: ```typescript import { init } from '@instantdb/react'; import schema from '../instant.schema.ts'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID, schema }); const roomId = 'hacker-chat-room-id'; // The `chat` room is typed automatically from schema! const room = db.room('chat', roomId); ``` Here's how we could add typesafety to our `chat` rooms: ```typescript // instant.schema.ts import { i } from '@instantdb/core'; const _schema = i.schema({ // ... rooms: { // 1. `chat` is the `roomType` chat: { // 2. Choose what presence looks like here presence: i.entity({ name: i.string(), status: i.string(), }), topics: { // 3. You can define payloads for different topics here sendEmoji: i.entity({ emoji: i.string(), }), }, }, }, }); // This helps TypeScript display better intellisense type _AppSchema = typeof _schema; interface AppSchema extends _AppSchema {} const schema: AppSchema = _schema; export type { AppSchema }; export default schema; ``` Once you've updated your schema, you'll start seeing types in your intellisense: ## Presence One common use case for presence is to show who's online. Instant's `usePresence` is similar in feel to `useState`. It returns an object containing the current user's presence state, the presence state of every other user in the room, and a function (`publishPresence`) to update the current user's presence. `publishPresence` is similar to React's `setState`, and will merge the current and new presence objects. ```tsx import { init } from '@instantdb/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); const room = db.room('chat', 'hacker-chat-room-id'); const randomId = Math.random().toString(36).slice(2, 6); const user = { name: `User#${randomId}`, }; function App() { const { user: myPresence, peers, publishPresence, } = db.rooms.usePresence( room, // Publish your presence when you join the room { initialPresence: { name: user.name } }, ); // Update your presence when your name changes useEffect(() => { publishPresence({ name: user.name }); }, [user.name]); if (!myPresence) { return

App loading...

; } return (

Who's online?

You are: {myPresence.name}

Others:

    {/* Loop through all peers and render their names. Peers will have the same properties as what you publish to the room. In this case, `name` is the only property we're publishing. Use RoomSchema to get type safety for your presence object. */} {Object.entries(peers).map(([peerId, peer]) => (
  • {peer.name}
  • ))}
); } ``` `usePresence` accepts a second parameter to select specific slices of the user's presence object. ```typescript const room = db.room('chat', 'hacker-chat-room-id'); // We only return the `status` value for each peer // We will _only_ trigger an update when a user's `status` value changes const { user, peers, publishPresence } = db.rooms.usePresence(room, { keys: ['status'], }); ``` You may also specify an array of `peers` and a `user` flag to further constrain the output. If you wanted a "write-only" hook, it would look like this: ```typescript // Will not trigger re-renders on presence changes const room = db.room('chat', 'hacker-chat-room-id'); const { publishPresence } = db.rooms.usePresence(room, { peers: [], user: false, }); ``` ## Topics Instant provides 2 hooks for sending and handling events for a given topic. `usePublishTopic` returns a function you can call to publish an event, and `useTopicEffect` will be called each time a peer in the same room publishes a topic event. Here's a live reaction feature using topics. You can also play with it live on [our recipes page](https://www.instantdb.com/recipes?#5-reactions) ```tsx 'use client'; import { init } from '@instantdb/react'; import { RefObject, createRef, useRef } from 'react'; // Instant app const APP_ID = '__APP_ID__'; // Set up room schema const emoji = { fire: '🔥', wave: '👋', confetti: '🎉', heart: '❤️', } as const; type EmojiName = keyof typeof emoji; const db = init({ appId: APP_ID, }); const room = db.room('main'); export default function InstantTopics() { // Use publishEmoji to broadcast to peers listening to `emoji` events. const publishEmoji = db.rooms.usePublishTopic(room, 'emoji'); // Use useTopicEffect to listen for `emoji` events from peers // and animate their emojis on the screen. db.rooms.useTopicEffect( room, 'emoji', ({ name, directionAngle, rotationAngle }) => { if (!emoji[name]) return; animateEmoji( { emoji: emoji[name], directionAngle, rotationAngle }, elRefsRef.current[name].current, ); }, ); const elRefsRef = useRef<{ [k: string]: RefObject; }>(refsInit); return (
{emojiNames.map((name) => (
))}
); } // Below are helper functions and styles used to animate the emojis const emojiNames = Object.keys(emoji) as EmojiName[]; const refsInit = Object.fromEntries( emojiNames.map((name) => [name, createRef()]), ); const containerClassNames = 'flex h-screen w-screen items-center justify-center overflow-hidden bg-gray-200 select-none'; const emojiButtonClassNames = 'rounded-lg bg-white p-3 text-3xl shadow-lg transition duration-200 ease-in-out hover:-translate-y-1 hover:shadow-xl'; function animateEmoji( config: { emoji: string; directionAngle: number; rotationAngle: number }, target: HTMLDivElement | null, ) { if (!target) return; const rootEl = document.createElement('div'); const directionEl = document.createElement('div'); const spinEl = document.createElement('div'); spinEl.innerText = config.emoji; directionEl.appendChild(spinEl); rootEl.appendChild(directionEl); target.appendChild(rootEl); style(rootEl, { transform: `rotate(${config.directionAngle * 360}deg)`, position: 'absolute', top: '0', left: '0', right: '0', bottom: '0', margin: 'auto', zIndex: '9999', pointerEvents: 'none', }); style(spinEl, { transform: `rotateZ(${config.rotationAngle * 400}deg)`, fontSize: `40px`, }); setTimeout(() => { style(directionEl, { transform: `translateY(40vh) scale(2)`, transition: 'all 400ms', opacity: '0', }); }, 20); setTimeout(() => rootEl.remove(), 800); } function style(el: HTMLElement, styles: Partial) { Object.assign(el.style, styles); } ``` ## Cursors and Typing Indicators (React only) We wanted to make adding real-time features to your apps as simple as possible, so we shipped our React library with 2 drop-in utilities: `Cursors` and `useTypingIndicator`. ### Cursors Adding multiplayer cursors to your app is as simple as importing our `` component! ```tsx 'use client'; import { init, Cursors } from '@instantdb/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); const room = db.room('chat', 'main'); export default function App() { return (
Open two tabs, and move your cursor around!
); } ``` You can provide a `renderCursor` function to return your own custom cursor component. ```tsx ``` You can render multiple cursor spaces. For instance, imagine you're building a screen with multiple tabs. You want to only show cursors on the same tab as the current user. You can provide each `` element with their own `spaceId`. ```tsx {tabs.map((tab) => ( {/* ... */} ))} ``` ### Typing indicators `useTypingIndicator` is a small utility useful for building inputs for chat-style apps. You can use this hook to show things like " is typing..." in your chat app. ```tsx 'use client'; import { init } from '@instantdb/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); const randomId = Math.random().toString(36).slice(2, 6); const user = { name: `User#${randomId}`, }; const room = db.room('chat', 'hacker-chat-room-id'); export default function InstantTypingIndicator() { // 1. Publish your presence in the room. db.rooms.useSyncPresence(room, user); // 2. Use the typing indicator hook const typing = db.rooms.useTypingIndicator(room, 'chat'); const onKeyDown = (e) => { // 3. Render typing indicator typing.inputProps.onKeyDown(e); // 4. Optionally run your own onKeyDown logic if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); console.log('Message sent:', e.target.value); } }; return (