Common mistakes
Below are some common mistakes when working with Instant.
#Common mistakes with schema
❌ Common mistake: Reusing the same label for different links
// ❌ Bad: Conflicting labelsconst _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 relationshipconst _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
// ❌ Bad: This will overwrite the entire preferences objectdb.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 datadb.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 objectdb.transact(db.tx.profiles[userId].update({preferences: {notifications: null}}));
✅ Correction: Use merge to remove keys from nested objects
// ✅ Good: Remove a nested keydb.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
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
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 operationsimport { 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 transactionsfor (let j = 0; j < batchSize && i + j < count; j++) {batch.push(db.tx.todos[id()].update({text: `Todo ${i + j}`,done: false}));}// Execute this batchawait db.transact(batch);}};// Create 1000 todos in batchescreateManyTodos(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 goalsconst query = { goals: {}, todos: {} };
✅ Correction: Nest namespaces to fetch associated entities
// ✅ Good: Fetch goals and their associated todosconst query = { goals: { todos: {} } };
Use where operator to filter entities:
❌ Common mistake: Placing where at the wrong level
// ❌ Bad: Filter must be inside $const query = {goals: {where: { id: 'goal-1' },},};
✅ Correction: Place where inside the $ operator
// ✅ Good: Fetch a specific goal by IDconst 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 titleconst 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
// ❌ 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
// ✅ Good: Find todos that are either high priority OR due soonconst 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
// ❌ Bad: Attribute must be indexed for comparison operatorsconst query = {todos: {$: {where: {nonIndexedAttr: { $gt: 5 }, // Will fail if attr isn't indexed},},},};
✅ Correction: Use comparison operators on indexed attributes
// ✅ Good: Find todos that take more than 2 hoursconst 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
// ❌ 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
// ✅ Good: Get first 10 todosconst query = {todos: {$: {limit: 10,},},};// ✅ Good: Get next 10 todosconst query = {todos: {$: {limit: 10,offset: 10,},},};
Use the order operator to sort results
❌ Common mistake: Using orderBy instead of order
// ❌ Bad: `orderBy` is not a valid operator. This will return an error!const query = {todos: {$: {orderBy: {serverCreatedAt: 'desc',},},},};
✅ Correction: Use order to sort results
// ✅ Good: Sort by creation time in descending orderconst query = {todos: {$: {order: {serverCreatedAt: 'desc',},},},};
❌ Common mistake: Ordering non-indexed fields
// ❌ Bad: Field must be indexed for orderingconst 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
// ❌ Bad: Don't use useQuery on the serverconst { data, isLoading, error } = db.useQuery({ todos: {} }); // Wrong approach!
✅ Correction: Use db.query in the admin SDK
// ✅ Good: Server-side queryingconst 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.
#Common mistakes with storage
Files in Instant are first-class entities ($files), not URLs. You link them to your data via the schema and query through the relationship to get URLs.
❌ Common mistake: Forgetting to declare $files in schema entities
If you use Storage, you must include $files in your schema entities. Without it, you will get a runtime error.
// ❌ Bad: Links reference $files but it's not declared in entitiesconst _schema = i.schema({entities: {posts: i.entity({caption: i.string(),}),},links: {postImage: {forward: { on: 'posts', has: 'one', label: 'image' },reverse: { on: '$files', has: 'many', label: 'posts' },},},});
✅ Correction: Declare $files in your schema entities
// ✅ Good: $files is declared in entitiesconst _schema = i.schema({entities: {$files: i.entity({path: i.string().unique().indexed(),url: i.string(),}),posts: i.entity({caption: i.string(),}),},links: {postImage: {forward: { on: 'posts', has: 'one', label: 'image' },reverse: { on: '$files', has: 'many', label: 'posts' },},},});
❌ Common mistake: Storing image URLs as string attributes
Do not store URLs as string attributes on your entities. This includes using placeholder image URLs (e.g. picsum.photos) in seed scripts. In a real app, users upload files via Storage, so string URLs won't work.
// ❌ Bad: Storing a URL string on the entityconst posts = [{ id: id(), caption: "Golden hour", image: "https://picsum.photos/seed/pier/600/600" },];db.transact(posts.map(p => db.tx.posts[p.id].update({ caption: p.caption, image: p.image })));// ❌ Also bad: querying the URL from a string attribute<img src={post.image} />
✅ Correction: Link $files to your entity and query through the relationship
// ✅ Good: Upload creates a $files entity, then link itconst postId = id();const { data } = await db.storage.uploadFile(`posts/${postId}/${file.name}`, file);db.transact(db.tx.posts[postId].update({ caption }).link({ image: data.id }));// Query through the relationship to get the URLconst { data } = db.useQuery({ posts: { image: {} } });<img src={post.image.url} />
❌ Common mistake: Creating $files via transactions
$files entities can only be created via db.storage.uploadFile. You cannot create them with db.transact, and you cannot set url via transactions.
// ❌ Bad: $files cannot be created or updated this waydb.transact(db.tx.$files[id()].update({path: 'photos/test.jpg',url: 'https://picsum.photos/200',}),);
✅ Correction: Use db.storage.uploadFile to create files
// ✅ Good: Upload creates the $files entityconst { data } = await db.storage.uploadFile('photos/test.jpg', file);// Then link itdb.transact(db.tx.posts[postId].link({ image: data.id }));