Pick your app

The examples below will be updated with your app ID.

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 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

// ❌ 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

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 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

// ❌ Bad: Filter must be inside $
const query = {
goals: {
where: { id: 'goal-1' },
},
};

Correction: Place where inside the $ operator

// ✅ 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

// ❌ 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 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

// ❌ 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

// ✅ 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

// ❌ 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 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

// ❌ 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 order
const query = {
todos: {
$: {
order: {
serverCreatedAt: 'desc',
},
},
},
};

Common mistake: Ordering non-indexed fields

// ❌ 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

// ❌ 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

// ✅ 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.

#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 entities
const _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 entities
const _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 entity
const 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 it
const 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 URL
const { 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 way
db.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 entity
const { data } = await db.storage.uploadFile('photos/test.jpg', file);
// Then link it
db.transact(db.tx.posts[postId].link({ image: data.id }));