{JSON.stringify(data, null, 2)} );
+```
+
+In the following sections we show how to use filters, joins, paginations.
+To keep these examples focused we won't show the `isLoading` and `error` states
+but these must be handled in actual code
+
+### Fetching an Entire Namespace
+
+To fetch all entities from a namespace, use an empty object without any
+operators.
+
+```typescript
+// ✅ Good: Fetch all goals
+const query = { goals: {} };
+const { data } = db.useQuery(query);
+
+// Result:
+// {
+// "goals": [
+// { "id": "goal-1", "title": "Get fit!" },
+// { "id": "goal-2", "title": "Get promoted!" }
+// ]
+// }
+```
+
+### Fetching Multiple Namespaces
+
+Query multiple namespaces in one go by specifying multiple namespaces:
+
+```typescript
+// ✅ Good: Fetch both goals and todos
+const query = { goals: {}, todos: {} };
+const { data } = db.useQuery(query);
+
+// Result:
+// {
+// "goals": [...],
+// "todos": [...]
+// }
+```
+
+❌ **Common mistake**: Nesting namespaces incorrectly
+
+```typescript
+// ❌ Bad: This will fetch todos associated with goals instead of all goals and
+todos
+const query = { goals: { todos: {} };
+```
+
+## Filtering
+
+### Fetching by ID
+
+Use `where` operator to filter entities:
+
+```typescript
+// ✅ Good: Fetch a specific goal by ID
+const query = {
+ goals: {
+ $: {
+ where: {
+ id: 'goal-1',
+ },
+ },
+ },
+};
+```
+
+❌ **Common mistake**: Placing filter at wrong level
+
+```typescript
+// ❌ Bad: Filter must be inside $
+const query = {
+ goals: {
+ where: { id: 'goal-1' },
+ },
+};
+```
+
+### Multiple Conditions
+
+Use multiple keys in `where` to filter with multiple conditions (AND logic):
+
+```typescript
+// ✅ Good: Fetch completed todos with high priority
+const query = {
+ todos: {
+ $: {
+ where: {
+ completed: true,
+ priority: 'high',
+ },
+ },
+ },
+};
+```
+
+## Associations (JOIN logic)
+
+### Fetching Related Entities
+
+Nest namespaces to fetch linked entities.
+
+```typescript
+// ✅ Good: Fetch goals with their related todos
+const query = {
+ goals: {
+ todos: {},
+ },
+};
+
+// Result:
+// {
+// "goals": [
+// {
+// "id": "goal-1",
+// "title": "Get fit!",
+// "todos": [
+// { "id": "todo-1", "title": "Go running" },
+// { "id": "todo-2", "title": "Eat healthy" }
+// ]
+// },
+// ...
+// ]
+// }
+```
+
+### Inverse Associations
+
+Links are bidirectional and you can query in the reverse direction
+
+```typescript
+// ✅ Good: Fetch todos with their related goals
+const query = {
+ todos: {
+ goals: {},
+ },
+};
+```
+
+### Filtering By Associations
+
+`where` operators support filtering entities based on associated values
+
+```typescript
+// ✅ Good: Find goals that have todos with a specific title
+const query = {
+ goals: {
+ $: {
+ where: {
+ 'todos.title': 'Go running',
+ },
+ },
+ todos: {},
+ },
+};
+```
+
+❌ **Common mistake**: Incorrect syntax for filtering on associated values
+
+```typescript
+// ❌ Bad: This will return an error!
+const query = {
+ goals: {
+ $: {
+ where: {
+ todos: { title: 'Go running' }, // Wrong: use dot notation instead
+ },
+ },
+ },
+};
+```
+
+### Filtering Associations
+
+You can use `where` in a nested namespace to filter out associated entities.
+
+```typescript
+// ✅ Good: Get goals with only their completed todos
+const query = {
+ goals: {
+ todos: {
+ $: {
+ where: {
+ completed: true,
+ },
+ },
+ },
+ },
+};
+```
+
+## Logical Operators
+
+### AND Operator
+
+Use `and` inside of `where` to filter associations based on multiple criteria
+
+```typescript
+// ✅ Good: Find goals with todos that are both high priority AND due soon
+const query = {
+ goals: {
+ $: {
+ where: {
+ and: [{ 'todos.priority': 'high' }, { 'todos.dueDate': { $lt: tomorrow } }],
+ },
+ },
+ },
+};
+```
+
+### OR Operator
+
+Use `or` inside of `where` to filter associated based on any criteria.
+
+```typescript
+// ✅ Good: Find todos that are either high priority OR due soon
+const query = {
+ todos: {
+ $: {
+ where: {
+ or: [{ priority: 'high' }, { dueDate: { $lt: tomorrow } }],
+ },
+ },
+ },
+};
+```
+
+❌ **Common mistake**: Incorrect synax 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
+ },
+ },
+ },
+};
+```
+
+### Comparison Operators
+
+Using `$gt`, `$lt`, `$gte`, or `$lte` is supported on indexed attributes with checked types:
+
+```typescript
+// ✅ Good: Find todos that take more than 2 hours
+const query = {
+ todos: {
+ $: {
+ where: {
+ timeEstimate: { $gt: 2 },
+ },
+ },
+ },
+};
+
+// Available operators: $gt, $lt, $gte, $lte
+```
+
+❌ **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
+ },
+ },
+ },
+};
+```
+
+### IN Operator
+
+Use `in` to match any value in a list:
+
+```typescript
+// ✅ Good: Find todos with specific priorities
+const query = {
+ todos: {
+ $: {
+ where: {
+ priority: { $in: ['high', 'critical'] },
+ },
+ },
+ },
+};
+```
+
+### NOT Operator
+
+Use `not` to match entities where an attribute doesn't equal a value:
+
+```typescript
+// ✅ Good: Find todos not assigned to "work" location
+const query = {
+ todos: {
+ $: {
+ where: {
+ location: { $not: 'work' },
+ },
+ },
+ },
+};
+```
+
+Note: This includes entities where the attribute is null or undefined.
+
+### NULL Check
+
+Use `$isNull` to match by null or undefined:
+
+```typescript
+// ✅ Good: Find todos with no assigned location
+const query = {
+ todos: {
+ $: {
+ where: {
+ location: { $isNull: true },
+ },
+ },
+ },
+};
+
+// ✅ Good: Find todos that have an assigned location
+const query = {
+ todos: {
+ $: {
+ where: {
+ location: { $isNull: false },
+ },
+ },
+ },
+};
+```
+
+### String Pattern Matching
+
+Use `$like` and `$ilike` to match on indexed string attributes:
+
+```typescript
+// ✅ Good: Find goals that start with "Get"
+const query = {
+ goals: {
+ $: {
+ where: {
+ title: { $like: 'Get%' }, // Case-sensitive
+ },
+ },
+ },
+};
+
+// For case-insensitive matching:
+const query = {
+ goals: {
+ $: {
+ where: {
+ title: { $ilike: 'get%' }, // Case-insensitive
+ },
+ },
+ },
+};
+```
+
+Pattern options:
+
+- `'prefix%'` - Starts with "prefix"
+- `'%suffix'` - Ends with "suffix"
+- `'%substring%'` - Contains "substring"
+
+## Pagination and Ordering
+
+### Limit and Offset
+
+Use `limit` and/or `offset` for simple pagination:
+
+```typescript
+// ✅ Good: Get first 10 todos
+const query = {
+ todos: {
+ $: {
+ limit: 10,
+ },
+ },
+};
+
+// ✅ Good: Get next 10 todos
+const query = {
+ todos: {
+ $: {
+ limit: 10,
+ offset: 10,
+ },
+ },
+};
+```
+
+❌ **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
+ },
+ },
+};
+```
+
+### Ordering
+
+Use the `order` operator to sort results
+
+```typescript
+// ✅ Good: Get todos sorted by dueDate
+const query = {
+ todos: {
+ $: {
+ order: {
+ dueDate: 'asc', // or 'desc'
+ },
+ },
+ },
+};
+
+// ✅ Good: Sort by creation time in descending order
+const query = {
+ todos: {
+ $: {
+ order: {
+ serverCreatedAt: 'desc',
+ },
+ },
+ },
+};
+```
+
+❌ **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',
+ },
+ },
+ },
+};
+```
+
+❌ **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
+ },
+ },
+ },
+};
+```
+
+## Field Selection
+
+Use the `fields` operator to select specific fields to optimize performance:
+
+```typescript
+// ✅ Good: Only fetch title and status fields
+const query = {
+ todos: {
+ $: {
+ fields: ['title', 'status'],
+ },
+ },
+};
+
+// Result will include the selected fields plus 'id' always:
+// {
+// "todos": [
+// { "id": "todo-1", "title": "Go running", "status": "completed" },
+// ...
+// ]
+// }
+```
+
+This works with nested associations too:
+
+```typescript
+// ✅ Good: Select different fields at different levels
+const query = {
+ goals: {
+ $: {
+ fields: ['title'],
+ },
+ todos: {
+ $: {
+ fields: ['status'],
+ },
+ },
+ },
+};
+```
+
+## Defer queries
+
+You can 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.
+
+```typescript
+const { isLoading, user, error } = db.useAuth();
+
+const {
+ isLoading: isLoadingTodos,
+ error,
+ data,
+} = db.useQuery(
+ user
+ ? {
+ // The query will run once user is populated
+ todos: {
+ $: {
+ where: {
+ userId: user.id,
+ },
+ },
+ },
+ }
+ : // Otherwise skip the query, which sets `isLoading` to true
+ null,
+);
+```
+
+## Combining Features
+
+You can combine these features to create powerful queries:
+
+```typescript
+// ✅ Good: Complex query combining multiple features
+const query = {
+ goals: {
+ $: {
+ where: {
+ or: [{ status: 'active' }, { 'todos.priority': 'high' }],
+ },
+ limit: 5,
+ order: { serverCreatedAt: 'desc' },
+ fields: ['title', 'description'],
+ },
+ todos: {
+ $: {
+ where: {
+ completed: false,
+ dueDate: { $lt: nextWeek },
+ },
+ fields: ['title', 'dueDate'],
+ },
+ },
+ },
+};
+```
+
+## Best Practices
+
+1. **Index fields in the schema** that you'll filter, sort, or use in comparisons
+2. **Use field selection** to minimize data transfer and re-renders
+3. **Defer queries** when dependent data isn't ready
+4. **Avoid deep nesting** of associations when possible
+5. **Be careful with queries** that might return large result sets, use where
+ clauses, limits, and pagination to avoid timeouts
+
+## Troubleshooting
+
+Common errors:
+
+1. **"Field must be indexed"**: Add an index to the field from the Explorer or schema
+2. **"Invalid operator"**: Check operator syntax and spelling
+3. **"Invalid query structure"**: Verify your query structure, especially $ placement
+
+# InstantDB User Management Guide
+
+This guide explains how to effectively manage users in your InstantDB applications, covering everything from basic user operations to advanced permission patterns.
+
+## Understanding the `$users` Namespace
+
+InstantDB provides a special system namespace called `$users` for managing user accounts. This namespace:
+
+- Is automatically created for every app
+- Contains basic user information (email, ID)
+- Has special rules and restrictions
+- Requires special handling in schemas and transactions
+
+## Default Permissions
+
+By default, the `$users` namespace has restrictive permissions:
+
+```typescript
+// Default permissions for $users
+{
+ $users: {
+ allow: {
+ view: 'auth.id == data.id', // Users can only view their own data
+ create: 'false', // Cannot create users directly
+ delete: 'false', // Cannot delete users directly
+ update: 'false', // Cannot update user properties directly
+ },
+ },
+}
+```
+
+These permissions ensure:
+
+- Users can only access their own user data
+- No direct modifications to the `$users` namespace
+- Authentication operations are handled securely
+
+## Extending User Data
+
+Since the `$users` namespace is read-only and can't be modified directly, you'll need to create additional namespaces and link them to users.
+
+❌ **Common mistake**: Using arrays instead of objects
+
+```typescript
+// ❌ Bad: Directly updating $users will throw an error!
+db.transact(db.tx.$users[userId].update({ nickname: 'Alice' }));
+```
+
+```
+// ✅ Good: Update linked profile instead
+db.transact(db.tx.profiles[profileId].update({ displayName: "Alice" }));
+```
+
+It's recommended to create a `profiles` namespace for storing additional user
+information.
+
+```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({
+ displayName: i.string(),
+ bio: i.string(),
+ avatarUrl: i.string(),
+ location: i.string(),
+ joinedAt: i.date().indexed(),
+ }),
+ },
+ links: {
+ userProfiles: {
+ // ✅ Good: Create link between profiles and $users
+ forward: { on: 'profiles', has: 'one', label: '$user' },
+ reverse: { on: '$users', has: 'one', label: 'profile' },
+ },
+ },
+});
+```
+
+❌ **Common mistake**: Placing `$users` in the forward direction
+
+```typescript
+// ❌ Bad: $users must be in the reverse direction
+userProfiles: {
+ forward: { on: '$users', has: 'one', label: 'profile' },
+ reverse: { on: 'profiles', has: 'one', label: '$user' },
+},
+```
+
+```typescript
+// lib/db.ts
+import { init } from '@instantdb/react';
+import schema from '../instant.schema';
+
+export const db = init({
+ appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!,
+ schema,
+});
+
+// app/page.tsx
+import { id } from '@instantdb/react';
+import { db } from '../lib/db';
+
+// ✅ Good: Create a profile for a new user
+async function createUserProfile(user) {
+ const profileId = id();
+ await db.transact(
+ db.tx.profiles[profileId]
+ .update({
+ displayName: user.email.split('@')[0], // Default name from email
+ bio: '',
+ joinedAt: new Date().toISOString(),
+ })
+ .link({ $user: user.id }), // Link to the user
+ );
+
+ return profileId;
+}
+```
+
+## Viewing all users
+
+The default permissions only allow users to view their own data. We recommend
+keeping it this way for security reasons. Instead of viewing all users, you can
+view all profiles
+
+```typescript
+// ✅ Good: View all profiles
+db.useQuery({ profiles: {} });
+```
+
+❌ **Common mistake**: Directly querying $users
+
+```typescript
+// ❌ Bad: This will likely only return the current user
+db.useQuery({ $users: {} });
+```
+
+## User Relationships
+
+You can model various relationships between users and other entities in your application.
+
+```typescript
+// ✅ Good: User posts relationship
+const _schema = i.schema({
+ entities: {
+ $users: i.entity({
+ email: i.string().unique().indexed(),
+ }),
+ profiles: i.entity({
+ displayName: i.string(),
+ bio: i.string(),
+ avatarUrl: i.string(),
+ location: i.string(),
+ joinedAt: i.date().indexed(),
+ }),
+ posts: i.entity({
+ title: i.string(),
+ content: i.string(),
+ createdAt: i.date().indexed(),
+ }),
+ },
+ links: {
+ userProfiles: {
+ forward: { on: 'profiles', has: 'one', label: '$user' },
+ reverse: { on: '$users', has: 'one', label: 'profile' },
+ },
+ postAuthor: {
+ forward: { on: 'posts', has: 'one', label: 'author' },
+ reverse: { on: 'profiles', has: 'many', label: 'posts' },
+ },
+ },
+});
+```
+
+Creating a post:
+
+```typescript
+// ✅ Good: Create a post linked to current user
+function createPost(title, content, currentProfile) {
+ const postId = id();
+ return db.transact(
+ db.tx.posts[postId]
+ .update({
+ title,
+ content,
+ createdAt: new Date().toISOString(),
+ })
+ .link({ author: currentProfile.id }),
+ );
+}
+```
+
+By linking `posts` to `profiles`, you can easily retrieve all posts by a user
+through their profile.
+
+```typescript
+// ✅ Good: Get all posts for a specific user
+// ... assuming currentProfile is already defined
+db.useQuery({
+ currentProfile
+ ? profiles: {
+ posts: {},
+ $: {
+ where: {
+ id: currentProfile.id
+ }
+ }
+ }
+ : null
+ }
+});
+```
+
+## Conclusion
+
+The `$users` namespace is a system generated namespace that lets you manage
+users in InstantDb.
+
+Key takeaways:
+
+1. The `$users` namespace is read-only and cannot be modified directly
+2. Always use linked entities to store additional user information
+3. When creating links, always put `$users` in the reverse direction
+
+# InstantDB Authentication Guide
+
+This guide explains how to implement user authentication in your InstantDB applications. InstantDB offers multiple authentication methods to suit different application needs and user preferences.
+
+## Authentication Options
+
+InstantDB supports several authentication methods, but use **Magic Code Authentication unless asked explicitly**.
+
+**Magic Code Authentication** - Email-based passwordless login
+
+## Core Authentication Concepts
+
+Before diving into specific methods, let's understand the key authentication concepts:
+
+### The `useAuth` Hook
+
+All authentication methods use the `useAuth` hook to access the current auth state:
+
+```javascript
+function App() {
+ const { isLoading, user, error } = db.useAuth();
+
+ if (isLoading) return;
+ if (error) return {JSON.stringify(data, null, 2)} );
+```
+
+In the following sections we show how to use filters, joins, paginations.
+To keep these examples focused we won't show the `isLoading` and `error` states
+but these must be handled in actual code
+
+### Fetching an Entire Namespace
+
+To fetch all entities from a namespace, use an empty object without any
+operators.
+
+```typescript
+// ✅ Good: Fetch all goals
+const query = { goals: {} };
+const { data } = db.useQuery(query);
+
+// Result:
+// {
+// "goals": [
+// { "id": "goal-1", "title": "Get fit!" },
+// { "id": "goal-2", "title": "Get promoted!" }
+// ]
+// }
+```
+
+### Fetching Multiple Namespaces
+
+Query multiple namespaces in one go by specifying multiple namespaces:
+
+```typescript
+// ✅ Good: Fetch both goals and todos
+const query = { goals: {}, todos: {} };
+const { data } = db.useQuery(query);
+
+// Result:
+// {
+// "goals": [...],
+// "todos": [...]
+// }
+```
+
+❌ **Common mistake**: Nesting namespaces incorrectly
+
+```typescript
+// ❌ Bad: This will fetch todos associated with goals instead of all goals and todos
+const query = { goals: { todos: {} };
+```
+
+## Filtering
+
+### Fetching by ID
+
+Use `where` operator to filter entities:
+
+```typescript
+// ✅ Good: Fetch a specific goal by ID
+const query = {
+ goals: {
+ $: {
+ where: {
+ id: 'goal-1',
+ },
+ },
+ },
+};
+```
+
+❌ **Common mistake**: Placing filter at wrong level
+
+```typescript
+// ❌ Bad: Filter must be inside $
+const query = {
+ goals: {
+ where: { id: 'goal-1' },
+ },
+};
+```
+
+### Multiple Conditions
+
+Use multiple keys in `where` to filter with multiple conditions (AND logic):
+
+```typescript
+// ✅ Good: Fetch completed todos with high priority
+const query = {
+ todos: {
+ $: {
+ where: {
+ completed: true,
+ priority: 'high',
+ },
+ },
+ },
+};
+```
+
+## Associations (JOIN logic)
+
+### Fetching Related Entities
+
+Nest namespaces to fetch linked entities.
+
+```typescript
+// ✅ Good: Fetch goals with their related todos
+const query = {
+ goals: {
+ todos: {},
+ },
+};
+
+// Result:
+// {
+// "goals": [
+// {
+// "id": "goal-1",
+// "title": "Get fit!",
+// "todos": [
+// { "id": "todo-1", "title": "Go running" },
+// { "id": "todo-2", "title": "Eat healthy" }
+// ]
+// },
+// ...
+// ]
+// }
+```
+
+### Inverse Associations
+
+Links are bidirectional and you can query in the reverse direction
+
+```typescript
+// ✅ Good: Fetch todos with their related goals
+const query = {
+ todos: {
+ goals: {},
+ },
+};
+```
+
+### Filtering By Associations
+
+`where` operators support filtering entities based on associated values
+
+```typescript
+// ✅ Good: Find goals that have todos with a specific title
+const query = {
+ goals: {
+ $: {
+ where: {
+ 'todos.title': 'Go running',
+ },
+ },
+ todos: {},
+ },
+};
+```
+
+❌ **Common mistake**: Incorrect syntax for filtering on associated values
+
+```typescript
+// ❌ Bad: This will return an error!
+const query = {
+ goals: {
+ $: {
+ where: {
+ todos: { title: 'Go running' }, // Wrong: use dot notation instead
+ },
+ },
+ },
+};
+```
+
+### Filtering Associations
+
+You can use `where` in a nested namespace to filter out associated entities.
+
+```typescript
+// ✅ Good: Get goals with only their completed todos
+const query = {
+ goals: {
+ todos: {
+ $: {
+ where: {
+ completed: true,
+ },
+ },
+ },
+ },
+};
+```
+
+## Logical Operators
+
+### AND Operator
+
+Use `and` inside of `where` to filter associations based on multiple criteria
+
+```typescript
+// ✅ Good: Find goals with todos that are both high priority AND due soon
+const query = {
+ goals: {
+ $: {
+ where: {
+ and: [{ 'todos.priority': 'high' }, { 'todos.dueDate': { $lt: tomorrow } }],
+ },
+ },
+ },
+};
+```
+
+### OR Operator
+
+Use `or` inside of `where` to filter associated based on any criteria.
+
+```typescript
+// ✅ Good: Find todos that are either high priority OR due soon
+const query = {
+ todos: {
+ $: {
+ where: {
+ or: [{ priority: 'high' }, { dueDate: { $lt: tomorrow } }],
+ },
+ },
+ },
+};
+```
+
+❌ **Common mistake**: Incorrect synax 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
+ },
+ },
+ },
+};
+```
+
+### Comparison Operators
+
+Using `$gt`, `$lt`, `$gte`, or `$lte` is supported on indexed attributes with checked types:
+
+```typescript
+// ✅ Good: Find todos that take more than 2 hours
+const query = {
+ todos: {
+ $: {
+ where: {
+ timeEstimate: { $gt: 2 },
+ },
+ },
+ },
+};
+
+// Available operators: $gt, $lt, $gte, $lte
+```
+
+❌ **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
+ },
+ },
+ },
+};
+```
+
+### IN Operator
+
+Use `in` to match any value in a list:
+
+```typescript
+// ✅ Good: Find todos with specific priorities
+const query = {
+ todos: {
+ $: {
+ where: {
+ priority: { $in: ['high', 'critical'] },
+ },
+ },
+ },
+};
+```
+
+### NOT Operator
+
+Use `not` to match entities where an attribute doesn't equal a value:
+
+```typescript
+// ✅ Good: Find todos not assigned to "work" location
+const query = {
+ todos: {
+ $: {
+ where: {
+ location: { $not: 'work' },
+ },
+ },
+ },
+};
+```
+
+Note: This includes entities where the attribute is null or undefined.
+
+### NULL Check
+
+Use `$isNull` to match by null or undefined:
+
+```typescript
+// ✅ Good: Find todos with no assigned location
+const query = {
+ todos: {
+ $: {
+ where: {
+ location: { $isNull: true },
+ },
+ },
+ },
+};
+
+// ✅ Good: Find todos that have an assigned location
+const query = {
+ todos: {
+ $: {
+ where: {
+ location: { $isNull: false },
+ },
+ },
+ },
+};
+```
+
+### String Pattern Matching
+
+Use `$like` and `$ilike` to match on indexed string attributes:
+
+```typescript
+// ✅ Good: Find goals that start with "Get"
+const query = {
+ goals: {
+ $: {
+ where: {
+ title: { $like: 'Get%' }, // Case-sensitive
+ },
+ },
+ },
+};
+
+// For case-insensitive matching:
+const query = {
+ goals: {
+ $: {
+ where: {
+ title: { $ilike: 'get%' }, // Case-insensitive
+ },
+ },
+ },
+};
+```
+
+Pattern options:
+
+- `'prefix%'` - Starts with "prefix"
+- `'%suffix'` - Ends with "suffix"
+- `'%substring%'` - Contains "substring"
+
+## Pagination and Ordering
+
+### Limit and Offset
+
+Use `limit` and/or `offset` for simple pagination:
+
+```typescript
+// ✅ Good: Get first 10 todos
+const query = {
+ todos: {
+ $: {
+ limit: 10,
+ },
+ },
+};
+
+// ✅ Good: Get next 10 todos
+const query = {
+ todos: {
+ $: {
+ limit: 10,
+ offset: 10,
+ },
+ },
+};
+```
+
+❌ **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
+ },
+ },
+};
+```
+
+### Ordering
+
+Use the `order` operator to sort results
+
+```typescript
+// ✅ Good: Get todos sorted by dueDate
+const query = {
+ todos: {
+ $: {
+ order: {
+ dueDate: 'asc', // or 'desc'
+ },
+ },
+ },
+};
+
+// ✅ Good: Sort by creation time in descending order
+const query = {
+ todos: {
+ $: {
+ order: {
+ serverCreatedAt: 'desc',
+ },
+ },
+ },
+};
+```
+
+❌ **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',
+ },
+ },
+ },
+};
+```
+
+❌ **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
+ },
+ },
+ },
+};
+```
+
+## Field Selection
+
+Use the `fields` operator to select specific fields to optimize performance:
+
+```typescript
+// ✅ Good: Only fetch title and status fields
+const query = {
+ todos: {
+ $: {
+ fields: ['title', 'status'],
+ },
+ },
+};
+
+// Result will include the selected fields plus 'id' always:
+// {
+// "todos": [
+// { "id": "todo-1", "title": "Go running", "status": "completed" },
+// ...
+// ]
+// }
+```
+
+This works with nested associations too:
+
+```typescript
+// ✅ Good: Select different fields at different levels
+const query = {
+ goals: {
+ $: {
+ fields: ['title'],
+ },
+ todos: {
+ $: {
+ fields: ['status'],
+ },
+ },
+ },
+};
+```
+
+## Defer queries
+
+You can 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.
+
+```typescript
+const { isLoading, user, error } = db.useAuth();
+
+const {
+ isLoading: isLoadingTodos,
+ error,
+ data,
+} = db.useQuery(
+ user
+ ? {
+ // The query will run once user is populated
+ todos: {
+ $: {
+ where: {
+ userId: user.id,
+ },
+ },
+ },
+ }
+ : // Otherwise skip the query, which sets `isLoading` to true
+ null,
+);
+```
+
+## Combining Features
+
+You can combine these features to create powerful queries:
+
+```typescript
+// ✅ Good: Complex query combining multiple features
+const query = {
+ goals: {
+ $: {
+ where: {
+ or: [{ status: 'active' }, { 'todos.priority': 'high' }],
+ },
+ limit: 5,
+ order: { serverCreatedAt: 'desc' },
+ fields: ['title', 'description'],
+ },
+ todos: {
+ $: {
+ where: {
+ completed: false,
+ dueDate: { $lt: nextWeek },
+ },
+ fields: ['title', 'dueDate'],
+ },
+ },
+ },
+};
+```
+
+## Best Practices
+
+1. **Index fields in the schema** that you'll filter, sort, or use in comparisons
+2. **Use field selection** to minimize data transfer and re-renders
+3. **Defer queries** when dependent data isn't ready
+4. **Avoid deep nesting** of associations when possible
+5. **Be careful with queries** that might return large result sets, use where
+ clauses, limits, and pagination to avoid timeouts
+
+## Troubleshooting
+
+Common errors:
+
+1. **"Field must be indexed"**: Add an index to the field from the Explorer or schema
+2. **"Invalid operator"**: Check operator syntax and spelling
+3. **"Invalid query structure"**: Verify your query structure, especially $ placement
+
+# InstantDB User Management Guide
+
+This guide explains how to effectively manage users in your InstantDB applications, covering everything from basic user operations to advanced permission patterns.
+
+## Understanding the `$users` Namespace
+
+InstantDB provides a special system namespace called `$users` for managing user accounts. This namespace:
+
+- Is automatically created for every app
+- Contains basic user information (email, ID)
+- Has special rules and restrictions
+- Requires special handling in schemas and transactions
+
+## Default Permissions
+
+By default, the `$users` namespace has restrictive permissions:
+
+```typescript
+// Default permissions for $users
+{
+ $users: {
+ allow: {
+ view: 'auth.id == data.id', // Users can only view their own data
+ create: 'false', // Cannot create users directly
+ delete: 'false', // Cannot delete users directly
+ update: 'false', // Cannot update user properties directly
+ },
+ },
+}
+```
+
+These permissions ensure:
+
+- Users can only access their own user data
+- No direct modifications to the `$users` namespace
+- Authentication operations are handled securely
+
+## Extending User Data
+
+Since the `$users` namespace is read-only and can't be modified directly, you'll need to create additional namespaces and link them to users.
+
+❌ **Common mistake**: Using arrays instead of objects
+
+```typescript
+// ❌ Bad: Directly updating $users will throw an error!
+db.transact(db.tx.$users[userId].update({ nickname: 'Alice' }));
+```
+
+```
+// ✅ Good: Update linked profile instead
+db.transact(db.tx.profiles[profileId].update({ displayName: "Alice" }));
+```
+
+It's recommended to create a `profiles` namespace for storing additional user
+information.
+
+```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({
+ displayName: i.string(),
+ bio: i.string(),
+ avatarUrl: i.string(),
+ location: i.string(),
+ joinedAt: i.date().indexed(),
+ }),
+ },
+ links: {
+ userProfiles: {
+ // ✅ Good: Create link between profiles and $users
+ forward: { on: 'profiles', has: 'one', label: '$user' },
+ reverse: { on: '$users', has: 'one', label: 'profile' },
+ },
+ },
+});
+```
+
+❌ **Common mistake**: Placing `$users` in the forward direction
+
+```typescript
+// ❌ Bad: $users must be in the reverse direction
+userProfiles: {
+ forward: { on: '$users', has: 'one', label: 'profile' },
+ reverse: { on: 'profiles', has: 'one', label: '$user' },
+},
+```
+
+```typescript
+// lib/db.ts
+import { init } from '@instantdb/react';
+import schema from '../instant.schema';
+
+export const db = init({
+ appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!,
+ schema,
+});
+
+// app/page.tsx
+import { id } from '@instantdb/react';
+import { db } from '../lib/db';
+
+// ✅ Good: Create a profile for a new user
+async function createUserProfile(user) {
+ const profileId = id();
+ await db.transact(
+ db.tx.profiles[profileId]
+ .update({
+ displayName: user.email.split('@')[0], // Default name from email
+ bio: '',
+ joinedAt: new Date().toISOString(),
+ })
+ .link({ $user: user.id }), // Link to the user
+ );
+
+ return profileId;
+}
+```
+
+## Viewing all users
+
+The default permissions only allow users to view their own data. We recommend
+keeping it this way for security reasons. Instead of viewing all users, you can
+view all profiles
+
+```typescript
+// ✅ Good: View all profiles
+db.useQuery({ profiles: {} });
+```
+
+❌ **Common mistake**: Directly querying $users
+
+```typescript
+// ❌ Bad: This will likely only return the current user
+db.useQuery({ $users: {} });
+```
+
+## User Relationships
+
+You can model various relationships between users and other entities in your application.
+
+```typescript
+// ✅ Good: User posts relationship
+const _schema = i.schema({
+ entities: {
+ $users: i.entity({
+ email: i.string().unique().indexed(),
+ }),
+ profiles: i.entity({
+ displayName: i.string(),
+ bio: i.string(),
+ avatarUrl: i.string(),
+ location: i.string(),
+ joinedAt: i.date().indexed(),
+ }),
+ posts: i.entity({
+ title: i.string(),
+ content: i.string(),
+ createdAt: i.date().indexed(),
+ }),
+ },
+ links: {
+ userProfiles: {
+ forward: { on: 'profiles', has: 'one', label: '$user' },
+ reverse: { on: '$users', has: 'one', label: 'profile' },
+ },
+ postAuthor: {
+ forward: { on: 'posts', has: 'one', label: 'author' },
+ reverse: { on: 'profiles', has: 'many', label: 'posts' },
+ },
+ },
+});
+```
+
+Creating a post:
+
+```typescript
+// ✅ Good: Create a post linked to current user
+function createPost(title, content, currentProfile) {
+ const postId = id();
+ return db.transact(
+ db.tx.posts[postId]
+ .update({
+ title,
+ content,
+ createdAt: new Date().toISOString(),
+ })
+ .link({ author: currentProfile.id }),
+ );
+}
+```
+
+By linking `posts` to `profiles`, you can easily retrieve all posts by a user
+through their profile.
+
+```typescript
+// ✅ Good: Get all posts for a specific user
+// ... assuming currentProfile is already defined
+db.useQuery({
+ currentProfile
+ ? profiles: {
+ posts: {},
+ $: {
+ where: {
+ id: currentProfile.id
+ }
+ }
+ }
+ : null
+ }
+});
+```
+
+## Conclusion
+
+The `$users` namespace is a system generated namespace that lets you manage
+users in InstantDb.
+
+Key takeaways:
+
+1. The `$users` namespace is read-only and cannot be modified directly
+2. Always use linked entities to store additional user information
+3. When creating links, always put `$users` in the reverse direction
+
+# InstantDB Authentication Guide
+
+This guide explains how to implement user authentication in your InstantDB applications. InstantDB offers multiple authentication methods to suit different application needs and user preferences.
+
+## Authentication Options
+
+InstantDB supports several authentication methods, but use **Magic Code Authentication unless asked explicitly**.
+
+**Magic Code Authentication** - Email-based passwordless login
+
+## Core Authentication Concepts
+
+Before diving into specific methods, let's understand the key authentication concepts:
+
+### The `useAuth` Hook
+
+All authentication methods use the `useAuth` hook to access the current auth state:
+
+```javascript
+function App() {
+ const { isLoading, user, error } = db.useAuth();
+
+ if (isLoading) return;
+ if (error) return {JSON.stringify(data, null, 2)} );
+```
+
+In the following sections we show how to use filters, joins, paginations.
+To keep these examples focused we won't show the `isLoading` and `error` states
+but these must be handled in actual code
+
+### Fetching an Entire Namespace
+
+To fetch all entities from a namespace, use an empty object without any
+operators.
+
+```typescript
+// ✅ Good: Fetch all goals
+const query = { goals: {} };
+const { data } = db.useQuery(query);
+
+// Result:
+// {
+// "goals": [
+// { "id": "goal-1", "title": "Get fit!" },
+// { "id": "goal-2", "title": "Get promoted!" }
+// ]
+// }
+```
+
+### Fetching Multiple Namespaces
+
+Query multiple namespaces in one go by specifying multiple namespaces:
+
+```typescript
+// ✅ Good: Fetch both goals and todos
+const query = { goals: {}, todos: {} };
+const { data } = db.useQuery(query);
+
+// Result:
+// {
+// "goals": [...],
+// "todos": [...]
+// }
+```
+
+❌ **Common mistake**: Nesting namespaces incorrectly
+
+```typescript
+// ❌ Bad: This will fetch todos associated with goals instead of all goals and todos
+const query = { goals: { todos: {} };
+```
+
+## Filtering
+
+### Fetching by ID
+
+Use `where` operator to filter entities:
+
+```typescript
+// ✅ Good: Fetch a specific goal by ID
+const query = {
+ goals: {
+ $: {
+ where: {
+ id: 'goal-1',
+ },
+ },
+ },
+};
+```
+
+❌ **Common mistake**: Placing filter at wrong level
+
+```typescript
+// ❌ Bad: Filter must be inside $
+const query = {
+ goals: {
+ where: { id: 'goal-1' },
+ },
+};
+```
+
+### Multiple Conditions
+
+Use multiple keys in `where` to filter with multiple conditions (AND logic):
+
+```typescript
+// ✅ Good: Fetch completed todos with high priority
+const query = {
+ todos: {
+ $: {
+ where: {
+ completed: true,
+ priority: 'high',
+ },
+ },
+ },
+};
+```
+
+## Associations (JOIN logic)
+
+### Fetching Related Entities
+
+Nest namespaces to fetch linked entities.
+
+```typescript
+// ✅ Good: Fetch goals with their related todos
+const query = {
+ goals: {
+ todos: {},
+ },
+};
+
+// Result:
+// {
+// "goals": [
+// {
+// "id": "goal-1",
+// "title": "Get fit!",
+// "todos": [
+// { "id": "todo-1", "title": "Go running" },
+// { "id": "todo-2", "title": "Eat healthy" }
+// ]
+// },
+// ...
+// ]
+// }
+```
+
+### Inverse Associations
+
+Links are bidirectional and you can query in the reverse direction
+
+```typescript
+// ✅ Good: Fetch todos with their related goals
+const query = {
+ todos: {
+ goals: {},
+ },
+};
+```
+
+### Filtering By Associations
+
+`where` operators support filtering entities based on associated values
+
+```typescript
+// ✅ Good: Find goals that have todos with a specific title
+const query = {
+ goals: {
+ $: {
+ where: {
+ 'todos.title': 'Go running',
+ },
+ },
+ todos: {},
+ },
+};
+```
+
+❌ **Common mistake**: Incorrect syntax for filtering on associated values
+
+```typescript
+// ❌ Bad: This will return an error!
+const query = {
+ goals: {
+ $: {
+ where: {
+ todos: { title: 'Go running' }, // Wrong: use dot notation instead
+ },
+ },
+ },
+};
+```
+
+### Filtering Associations
+
+You can use `where` in a nested namespace to filter out associated entities.
+
+```typescript
+// ✅ Good: Get goals with only their completed todos
+const query = {
+ goals: {
+ todos: {
+ $: {
+ where: {
+ completed: true,
+ },
+ },
+ },
+ },
+};
+```
+
+## Logical Operators
+
+### AND Operator
+
+Use `and` inside of `where` to filter associations based on multiple criteria
+
+```typescript
+// ✅ Good: Find goals with todos that are both high priority AND due soon
+const query = {
+ goals: {
+ $: {
+ where: {
+ and: [{ 'todos.priority': 'high' }, { 'todos.dueDate': { $lt: tomorrow } }],
+ },
+ },
+ },
+};
+```
+
+### OR Operator
+
+Use `or` inside of `where` to filter associated based on any criteria.
+
+```typescript
+// ✅ Good: Find todos that are either high priority OR due soon
+const query = {
+ todos: {
+ $: {
+ where: {
+ or: [{ priority: 'high' }, { dueDate: { $lt: tomorrow } }],
+ },
+ },
+ },
+};
+```
+
+❌ **Common mistake**: Incorrect synax 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
+ },
+ },
+ },
+};
+```
+
+### Comparison Operators
+
+Using `$gt`, `$lt`, `$gte`, or `$lte` is supported on indexed attributes with checked types:
+
+```typescript
+// ✅ Good: Find todos that take more than 2 hours
+const query = {
+ todos: {
+ $: {
+ where: {
+ timeEstimate: { $gt: 2 },
+ },
+ },
+ },
+};
+
+// Available operators: $gt, $lt, $gte, $lte
+```
+
+❌ **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
+ },
+ },
+ },
+};
+```
+
+### IN Operator
+
+Use `in` to match any value in a list:
+
+```typescript
+// ✅ Good: Find todos with specific priorities
+const query = {
+ todos: {
+ $: {
+ where: {
+ priority: { $in: ['high', 'critical'] },
+ },
+ },
+ },
+};
+```
+
+### NOT Operator
+
+Use `not` to match entities where an attribute doesn't equal a value:
+
+```typescript
+// ✅ Good: Find todos not assigned to "work" location
+const query = {
+ todos: {
+ $: {
+ where: {
+ location: { $not: 'work' },
+ },
+ },
+ },
+};
+```
+
+Note: This includes entities where the attribute is null or undefined.
+
+### NULL Check
+
+Use `$isNull` to match by null or undefined:
+
+```typescript
+// ✅ Good: Find todos with no assigned location
+const query = {
+ todos: {
+ $: {
+ where: {
+ location: { $isNull: true },
+ },
+ },
+ },
+};
+
+// ✅ Good: Find todos that have an assigned location
+const query = {
+ todos: {
+ $: {
+ where: {
+ location: { $isNull: false },
+ },
+ },
+ },
+};
+```
+
+### String Pattern Matching
+
+Use `$like` and `$ilike` to match on indexed string attributes:
+
+```typescript
+// ✅ Good: Find goals that start with "Get"
+const query = {
+ goals: {
+ $: {
+ where: {
+ title: { $like: 'Get%' }, // Case-sensitive
+ },
+ },
+ },
+};
+
+// For case-insensitive matching:
+const query = {
+ goals: {
+ $: {
+ where: {
+ title: { $ilike: 'get%' }, // Case-insensitive
+ },
+ },
+ },
+};
+```
+
+Pattern options:
+
+- `'prefix%'` - Starts with "prefix"
+- `'%suffix'` - Ends with "suffix"
+- `'%substring%'` - Contains "substring"
+
+## Pagination and Ordering
+
+### Limit and Offset
+
+Use `limit` and/or `offset` for simple pagination:
+
+```typescript
+// ✅ Good: Get first 10 todos
+const query = {
+ todos: {
+ $: {
+ limit: 10,
+ },
+ },
+};
+
+// ✅ Good: Get next 10 todos
+const query = {
+ todos: {
+ $: {
+ limit: 10,
+ offset: 10,
+ },
+ },
+};
+```
+
+❌ **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
+ },
+ },
+};
+```
+
+### Ordering
+
+Use the `order` operator to sort results
+
+```typescript
+// ✅ Good: Get todos sorted by dueDate
+const query = {
+ todos: {
+ $: {
+ order: {
+ dueDate: 'asc', // or 'desc'
+ },
+ },
+ },
+};
+
+// ✅ Good: Sort by creation time in descending order
+const query = {
+ todos: {
+ $: {
+ order: {
+ serverCreatedAt: 'desc',
+ },
+ },
+ },
+};
+```
+
+❌ **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',
+ },
+ },
+ },
+};
+```
+
+❌ **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
+ },
+ },
+ },
+};
+```
+
+## Defer queries
+
+You can 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.
+
+```typescript
+const { isLoading, user, error } = db.useAuth();
+
+const {
+ isLoading: isLoadingTodos,
+ error,
+ data,
+} = db.useQuery(
+ user
+ ? {
+ // The query will run once user is populated
+ todos: {
+ $: {
+ where: {
+ userId: user.id,
+ },
+ },
+ },
+ }
+ : // Otherwise skip the query, which sets `isLoading` to true
+ null,
+);
+```
+
+## Best Practices
+
+1. **Index fields in the schema** that you'll filter, sort, or use in comparisons
+2. **Use field selection** to minimize data transfer and re-renders
+3. **Defer queries** when dependent data isn't ready
+4. **Avoid deep nesting** of associations when possible
+5. **Be careful with queries** that might return large result sets, use where
+ clauses, limits, and pagination to avoid timeouts
+
+## Troubleshooting
+
+Common errors:
+
+1. **"Field must be indexed"**: Add an index to the field from the Explorer or schema
+2. **"Invalid operator"**: Check operator syntax and spelling
+3. **"Invalid query structure"**: Verify your query structure, especially $ placement
+
+# InstantDB User Management Guide
+
+This guide explains how to effectively manage users in your InstantDB applications, covering everything from basic user operations to advanced permission patterns.
+
+## Understanding the `$users` Namespace
+
+InstantDB provides a special system namespace called `$users` for managing user accounts. This namespace:
+
+- Is automatically created for every app
+- Contains basic user information (email, ID)
+- Has special rules and restrictions
+- Requires special handling in schemas and transactions
+
+## Default Permissions
+
+By default, the `$users` namespace has restrictive permissions:
+
+```typescript
+// Default permissions for $users
+{
+ $users: {
+ allow: {
+ view: 'auth.id == data.id', // Users can only view their own data
+ create: 'false', // Cannot create users directly
+ delete: 'false', // Cannot delete users directly
+ update: 'false', // Cannot update user properties directly
+ },
+ },
+}
+```
+
+These permissions ensure:
+
+- Users can only access their own user data
+- No direct modifications to the `$users` namespace
+- Authentication operations are handled securely
+
+## Extending User Data
+
+Since the `$users` namespace is read-only and can't be modified directly, you'll need to create additional namespaces and link them to users.
+
+❌ **Common mistake**: Using arrays instead of objects
+
+```typescript
+// ❌ Bad: Directly updating $users will throw an error!
+db.transact(db.tx.$users[userId].update({ nickname: 'Alice' }));
+```
+
+```
+// ✅ Good: Update linked profile instead
+db.transact(db.tx.profiles[profileId].update({ displayName: "Alice" }));
+```
+
+It's recommended to create a `profiles` namespace for storing additional user
+information.
+
+```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({
+ displayName: i.string(),
+ bio: i.string(),
+ avatarUrl: i.string(),
+ location: i.string(),
+ joinedAt: i.date().indexed(),
+ }),
+ },
+ links: {
+ userProfiles: {
+ // ✅ Good: Create link between profiles and $users
+ forward: { on: 'profiles', has: 'one', label: '$user' },
+ reverse: { on: '$users', has: 'one', label: 'profile' },
+ },
+ },
+});
+```
+
+❌ **Common mistake**: Placing `$users` in the forward direction
+
+```typescript
+// ❌ Bad: $users must be in the reverse direction
+userProfiles: {
+ forward: { on: '$users', has: 'one', label: 'profile' },
+ reverse: { on: 'profiles', has: 'one', label: '$user' },
+},
+```
+
+```typescript
+// lib/db.ts
+import { init } from '@instantdb/react';
+import schema from '../instant.schema';
+
+export const db = init({
+ appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!,
+ schema,
+});
+
+// app/page.tsx
+import { id } from '@instantdb/react';
+import { db } from '../lib/db';
+
+// ✅ Good: Create a profile for a new user
+async function createUserProfile(user) {
+ const profileId = id();
+ await db.transact(
+ db.tx.profiles[profileId]
+ .update({
+ displayName: user.email.split('@')[0], // Default name from email
+ bio: '',
+ joinedAt: new Date().toISOString(),
+ })
+ .link({ $user: user.id }), // Link to the user
+ );
+
+ return profileId;
+}
+```
+
+## Viewing all users
+
+The default permissions only allow users to view their own data. We recommend
+keeping it this way for security reasons. Instead of viewing all users, you can
+view all profiles
+
+```typescript
+// ✅ Good: View all profiles
+db.useQuery({ profiles: {} });
+```
+
+❌ **Common mistake**: Directly querying $users
+
+```typescript
+// ❌ Bad: This will likely only return the current user
+db.useQuery({ $users: {} });
+```
+
+## User Relationships
+
+You can model various relationships between users and other entities in your application.
+
+```typescript
+// ✅ Good: User posts relationship
+const _schema = i.schema({
+ entities: {
+ $users: i.entity({
+ email: i.string().unique().indexed(),
+ }),
+ profiles: i.entity({
+ displayName: i.string(),
+ bio: i.string(),
+ avatarUrl: i.string(),
+ location: i.string(),
+ joinedAt: i.date().indexed(),
+ }),
+ posts: i.entity({
+ title: i.string(),
+ content: i.string(),
+ createdAt: i.date().indexed(),
+ }),
+ },
+ links: {
+ userProfiles: {
+ forward: { on: 'profiles', has: 'one', label: '$user' },
+ reverse: { on: '$users', has: 'one', label: 'profile' },
+ },
+ postAuthor: {
+ forward: { on: 'posts', has: 'one', label: 'author' },
+ reverse: { on: 'profiles', has: 'many', label: 'posts' },
+ },
+ },
+});
+```
+
+Creating a post:
+
+```typescript
+// ✅ Good: Create a post linked to current user
+function createPost(title, content, currentProfile) {
+ const postId = id();
+ return db.transact(
+ db.tx.posts[postId]
+ .update({
+ title,
+ content,
+ createdAt: new Date().toISOString(),
+ })
+ .link({ author: currentProfile.id }),
+ );
+}
+```
+
+By linking `posts` to `profiles`, you can easily retrieve all posts by a user
+through their profile.
+
+```typescript
+// ✅ Good: Get all posts for a specific user
+// ... assuming currentProfile is already defined
+db.useQuery({
+ currentProfile
+ ? profiles: {
+ posts: {},
+ $: {
+ where: {
+ id: currentProfile.id
+ }
+ }
+ }
+ : null
+ }
+});
+```
+
+## Conclusion
+
+The `$users` namespace is a system generated namespace that lets you manage
+users in InstantDb.
+
+Key takeaways:
+
+1. The `$users` namespace is read-only and cannot be modified directly
+2. Always use linked entities to store additional user information
+3. When creating links, always put `$users` in the reverse direction
+
+# InstantDB Authentication Guide
+
+This guide explains how to implement user authentication in your InstantDB applications. InstantDB offers multiple authentication methods to suit different application needs and user preferences.
+
+## Authentication Options
+
+InstantDB supports several authentication methods, but use **Magic Code Authentication unless asked explicitly**.
+
+**Magic Code Authentication** - Email-based passwordless login
+
+## Core Authentication Concepts
+
+Before diving into specific methods, let's understand the key authentication concepts:
+
+### The `useAuth` Hook
+
+All authentication methods use the `useAuth` hook to access the current auth state:
+
+```javascript
+function App() {
+ const { isLoading, user, error } = db.useAuth();
+
+ if (isLoading) return;
+ if (error) return {JSON.stringify(data, null, 2)} );
+```
+
+In the following sections we show how to use filters, joins, paginations.
+To keep these examples focused we won't show the `isLoading` and `error` states
+but these must be handled in actual code
+
+### Fetching an Entire Namespace
+
+To fetch all entities from a namespace, use an empty object without any
+operators.
+
+```typescript
+// ✅ Good: Fetch all goals
+const query = { goals: {} };
+const { data } = db.useQuery(query);
+
+// Result:
+// {
+// "goals": [
+// { "id": "goal-1", "title": "Get fit!" },
+// { "id": "goal-2", "title": "Get promoted!" }
+// ]
+// }
+```
+
+### Fetching Multiple Namespaces
+
+Query multiple namespaces in one go by specifying mulitple namespaces:
+
+```typescript
+// ✅ Good: Fetch both goals and todos
+const query = { goals: {}, todos: {} };
+const { data } = db.useQuery(query);
+
+// Result:
+// {
+// "goals": [...],
+// "todos": [...]
+// }
+```
+
+❌ **Common mistake**: Nesting namespaces incorrectly
+
+```typescript
+// ❌ Bad: This will fetch todos associated with goals instead of all goals and
+todos
+const query = { goals: { todos: {} };
+```
+
+## Filtering
+
+### Fetching by ID
+
+Use `where` operator to filter entities:
+
+```typescript
+// ✅ Good: Fetch a specific goal by ID
+const query = {
+ goals: {
+ $: {
+ where: {
+ id: 'goal-1',
+ },
+ },
+ },
+};
+```
+
+❌ **Common mistake**: Placing filter at wrong level
+
+```typescript
+// ❌ Bad: Filter must be inside $
+const query = {
+ goals: {
+ where: { id: 'goal-1' },
+ },
+};
+```
+
+### Multiple Conditions
+
+Use multiple keys in `where` to filter with multiple conditions (AND logic):
+
+```typescript
+// ✅ Good: Fetch completed todos with high priority
+const query = {
+ todos: {
+ $: {
+ where: {
+ completed: true,
+ priority: 'high',
+ },
+ },
+ },
+};
+```
+
+## Associations (JOIN logic)
+
+### Fetching Related Entities
+
+Nest namespaces to fetch linked entities.
+
+```typescript
+// ✅ Good: Fetch goals with their related todos
+const query = {
+ goals: {
+ todos: {},
+ },
+};
+
+// Result:
+// {
+// "goals": [
+// {
+// "id": "goal-1",
+// "title": "Get fit!",
+// "todos": [
+// { "id": "todo-1", "title": "Go running" },
+// { "id": "todo-2", "title": "Eat healthy" }
+// ]
+// },
+// ...
+// ]
+// }
+```
+
+### Inverse Associations
+
+Links are bidirectional and you can query in the reverse direction
+
+```typescript
+// ✅ Good: Fetch todos with their related goals
+const query = {
+ todos: {
+ goals: {},
+ },
+};
+```
+
+### Filtering By Associations
+
+`where` operators support filtering entities based on associated values
+
+```typescript
+// ✅ Good: Find goals that have todos with a specific title
+const query = {
+ goals: {
+ $: {
+ where: {
+ 'todos.title': 'Go running',
+ },
+ },
+ todos: {},
+ },
+};
+```
+
+❌ **Common mistake**: Incorrect syntax for filtering on associated values
+
+```typescript
+// ❌ Bad: This will return an error!
+const query = {
+ goals: {
+ $: {
+ where: {
+ todos: { title: 'Go running' }, // Wrong: use dot notation instead
+ },
+ },
+ },
+};
+```
+
+### Filtering Associations
+
+You can use `where` in a nested namespace to filter out associated entities.
+
+```typescript
+// ✅ Good: Get goals with only their completed todos
+const query = {
+ goals: {
+ todos: {
+ $: {
+ where: {
+ completed: true,
+ },
+ },
+ },
+ },
+};
+```
+
+## Logical Operators
+
+### AND Operator
+
+Use `and` inside of `where` to filter associations based on multiple criteria
+
+```typescript
+// ✅ Good: Find goals with todos that are both high priority AND due soon
+const query = {
+ goals: {
+ $: {
+ where: {
+ and: [{ 'todos.priority': 'high' }, { 'todos.dueDate': { $lt: tomorrow } }],
+ },
+ },
+ },
+};
+```
+
+### OR Operator
+
+Use `or` inside of `where` to filter associated based on any criteria.
+
+```typescript
+// ✅ Good: Find todos that are either high priority OR due soon
+const query = {
+ todos: {
+ $: {
+ where: {
+ or: [{ priority: 'high' }, { dueDate: { $lt: tomorrow } }],
+ },
+ },
+ },
+};
+```
+
+❌ **Common mistake**: Incorrect synax 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
+ },
+ },
+ },
+};
+```
+
+### Comparison Operators
+
+Using `$gt`, `$lt`, `$gte`, or `$lte` is supported on indexed attributes with checked types:
+
+```typescript
+// ✅ Good: Find todos that take more than 2 hours
+const query = {
+ todos: {
+ $: {
+ where: {
+ timeEstimate: { $gt: 2 },
+ },
+ },
+ },
+};
+
+// Available operators: $gt, $lt, $gte, $lte
+```
+
+❌ **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
+ },
+ },
+ },
+};
+```
+
+### IN Operator
+
+Use `in` to match any value in a list:
+
+```typescript
+// ✅ Good: Find todos with specific priorities
+const query = {
+ todos: {
+ $: {
+ where: {
+ priority: { $in: ['high', 'critical'] },
+ },
+ },
+ },
+};
+```
+
+### NOT Operator
+
+Use `not` to match entities where an attribute doesn't equal a value:
+
+```typescript
+// ✅ Good: Find todos not assigned to "work" location
+const query = {
+ todos: {
+ $: {
+ where: {
+ location: { $not: 'work' },
+ },
+ },
+ },
+};
+```
+
+Note: This includes entities where the attribute is null or undefined.
+
+### NULL Check
+
+Use `$isNull` to match by null or undefined:
+
+```typescript
+// ✅ Good: Find todos with no assigned location
+const query = {
+ todos: {
+ $: {
+ where: {
+ location: { $isNull: true },
+ },
+ },
+ },
+};
+
+// ✅ Good: Find todos that have an assigned location
+const query = {
+ todos: {
+ $: {
+ where: {
+ location: { $isNull: false },
+ },
+ },
+ },
+};
+```
+
+### String Pattern Matching
+
+Use `$like` and `$ilike` to match on indexed string attributes:
+
+```typescript
+// ✅ Good: Find goals that start with "Get"
+const query = {
+ goals: {
+ $: {
+ where: {
+ title: { $like: 'Get%' }, // Case-sensitive
+ },
+ },
+ },
+};
+
+// For case-insensitive matching:
+const query = {
+ goals: {
+ $: {
+ where: {
+ title: { $ilike: 'get%' }, // Case-insensitive
+ },
+ },
+ },
+};
+```
+
+Pattern options:
+
+- `'prefix%'` - Starts with "prefix"
+- `'%suffix'` - Ends with "suffix"
+- `'%substring%'` - Contains "substring"
+
+## Pagination and Ordering
+
+### Limit and Offset
+
+Use `limit` and/or `offset` for simple pagination:
+
+```typescript
+// ✅ Good: Get first 10 todos
+const query = {
+ todos: {
+ $: {
+ limit: 10,
+ },
+ },
+};
+
+// ✅ Good: Get next 10 todos
+const query = {
+ todos: {
+ $: {
+ limit: 10,
+ offset: 10,
+ },
+ },
+};
+```
+
+❌ **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
+ },
+ },
+};
+```
+
+### Ordering
+
+Use the `order` operator to sort results
+
+```typescript
+// ✅ Good: Get todos sorted by dueDate
+const query = {
+ todos: {
+ $: {
+ order: {
+ dueDate: 'asc', // or 'desc'
+ },
+ },
+ },
+};
+
+// ✅ Good: Sort by creation time in descending order
+const query = {
+ todos: {
+ $: {
+ order: {
+ serverCreatedAt: 'desc',
+ },
+ },
+ },
+};
+```
+
+❌ **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',
+ },
+ },
+ },
+};
+```
+
+❌ **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
+ },
+ },
+ },
+};
+```
+
+## Field Selection
+
+Use the `fields` operator to select specific fields to optimize performance:
+
+```typescript
+// ✅ Good: Only fetch title and status fields
+const query = {
+ todos: {
+ $: {
+ fields: ['title', 'status'],
+ },
+ },
+};
+
+// Result will include the selected fields plus 'id' always:
+// {
+// "todos": [
+// { "id": "todo-1", "title": "Go running", "status": "completed" },
+// ...
+// ]
+// }
+```
+
+This works with nested associations too:
+
+```typescript
+// ✅ Good: Select different fields at different levels
+const query = {
+ goals: {
+ $: {
+ fields: ['title'],
+ },
+ todos: {
+ $: {
+ fields: ['status'],
+ },
+ },
+ },
+};
+```
+
+## Defer queries
+
+You can 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.
+
+```typescript
+const { isLoading, user, error } = db.useAuth();
+
+const {
+ isLoading: isLoadingTodos,
+ error,
+ data,
+} = db.useQuery(
+ user
+ ? {
+ // The query will run once user is populated
+ todos: {
+ $: {
+ where: {
+ userId: user.id,
+ },
+ },
+ },
+ }
+ : // Otherwise skip the query, which sets `isLoading` to true
+ null,
+);
+```
+
+## Combining Features
+
+You can combine these features to create powerful queries:
+
+```typescript
+// ✅ Good: Complex query combining multiple features
+const query = {
+ goals: {
+ $: {
+ where: {
+ or: [{ status: 'active' }, { 'todos.priority': 'high' }],
+ },
+ limit: 5,
+ order: { serverCreatedAt: 'desc' },
+ fields: ['title', 'description'],
+ },
+ todos: {
+ $: {
+ where: {
+ completed: false,
+ dueDate: { $lt: nextWeek },
+ },
+ fields: ['title', 'dueDate'],
+ },
+ },
+ },
+};
+```
+
+## Best Practices
+
+1. **Index fields in the schema** that you'll filter, sort, or use in comparisons
+2. **Use field selection** to minimize data transfer and re-renders
+3. **Defer queries** when dependent data isn't ready
+4. **Avoid deep nesting** of associations when possible
+5. **Be careful with queries** that might return large result sets, use where
+ clauses, limits, and pagination to avoid timeouts
+
+## Troubleshooting
+
+Common errors:
+
+1. **"Field must be indexed"**: Add an index to the field from the Explorer or schema
+2. **"Invalid operator"**: Check operator syntax and spelling
+3. **"Invalid query structure"**: Verify your query structure, especially $ placement
+
+# InstantDB Server-Side Development Guide
+
+This guide explains how to use InstantDB in server-side javascript environments
+
+## Initializing the Admin SDK
+
+For server-side operations, Instant exposes `@instantdb/admin`. This package has similar functionality to the client SDK but is designed specifically for server environments.
+
+First, install the admin SDK:
+
+```bash
+npm install @instantdb/admin
+```
+
+Now you can use it in your project
+
+```javascript
+// ✅ Good: Proper server-side initialization
+import { init, id } from '@instantdb/admin';
+
+const db = init({
+ appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID,
+ adminToken: process.env.INSTANT_APP_ADMIN_TOKEN,
+});
+```
+
+❌ **Common mistake**: Using client SDK on the server
+
+```javascript
+// ❌ Bad: Don't use the React SDK on the server
+import { init } from '@instantdb/react'; // Wrong package!
+
+const db = init({
+ appId: process.env.INSTANT_APP_ID,
+ adminToken: process.env.INSTANT_APP_ADMIN_TOKEN,
+});
+```
+
+Hardcoding or exposing your app id is fine but make sure to never expose
+your admin token.
+
+❌ **Common mistake**: Exposing admin token in client code
+
+```javascript
+// ❌ Bad: Never expose your admin token in client code
+const db = init({
+ appId: 'app-123',
+ adminToken: 'admin-token-abc', // Hardcoded token = security risk!
+});
+```
+
+For better type safety, include your schema:
+
+```javascript
+// ✅ Good: Using schema for type safety
+import { init, id } from '@instantdb/admin';
+import schema from '../instant.schema'; // Your schema file
+
+const db = init({
+ appId: process.env.INSTANT_APP_ID,
+ adminToken: process.env.INSTANT_APP_ADMIN_TOKEN,
+ schema, // Add your schema here
+});
+```
+
+## Reading Data from the Server
+
+The structure of queries from the admin sdk is identical to the client SDK
+
+```typescript
+{
+ namespace: {
+ $: { /* operators for this namespace */ },
+ linkedNamespace: {
+ $: { /* operators for this linked namespace */ },
+ },
+ },
+}
+```
+
+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
+
+```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 mistake**: Using client-side syntax
+
+```javascript
+// ❌ Bad: Don't use useQuery on the server
+const { data, isLoading, error } = db.useQuery({ todos: {} }); // Wrong approach!
+```
+
+## Writing Data from the Server
+
+Use `db.transact` in the admin SDK to create, update, and delete data.
+`db.transact` has the same API and behaves the same in the admin and client SDK.
+The only difference is permission checks are bypassed in the admin SDK.
+
+```javascript
+// ✅ Good: Server-side transaction
+const createTodo = async (title, dueDate) => {
+ try {
+ const result = await db.transact(
+ db.tx.todos[id()].update({
+ title,
+ dueDate,
+ createdAt: new Date().toISOString(),
+ completed: false,
+ }),
+ );
+
+ console.log('Created todo with transaction ID:', result['tx-id']);
+ return result;
+ } catch (error) {
+ console.error('Error creating todo:', error);
+ throw error;
+ }
+};
+```
+
+## Impersonate a User
+
+Ue `db.asUser` to enforce permission checks for queries and transactions. This
+is **ONLY** available in the admin SDK.
+
+```typescript
+// ✅ Good: Impersonating a user by email
+const userDb = db.asUser({ email: userEmail });
+
+// ✅ Good: Impersonating a user with a token
+const userDb = db.asUser({ token: userToken });
+
+// ✅ Good: Operating as a guest
+const guestDb = db.asUser({ guest: true });
+};
+```
+
+## Retrieve a user
+
+Use `db.auth.getUser` to retrieve an app user. This is \*_ONLY_ available in the admin SDk
+
+```typescript
+// ✅ Good: Retrieve a user by email
+const user = await db.auth.getUser({ email: 'alyssa_p_hacker@instantdb.com' });
+
+// ✅ Good: Retrieve a user by id
+const user = await db.auth.getUser({ id: userId });
+
+// ✅ Good: Retrieve a user by refresh_token.
+const user = await db.auth.getUser({ refresh_token: userRefreshToken });
+```
+
+## Delete a user
+
+Use `db.auth.deleteUser` to delete an app user. This is \*_ONLY_ available in the admin SDk
+
+```typescript
+// ✅ Good: Delete a user by email
+const user = await db.auth.deleteUser({ email: 'alyssa_p_hacker@instantdb.com' });
+
+// ✅ Good: Delete a user by id
+const user = await db.auth.deleteUser({ id: userId });
+
+// ✅ Good: Delete a user by refresh_token.
+const user = 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 to delete you need to do an additional transaction.
+
+## Sign Out Users
+
+Use `db.auth.signOut(email: string)` to sign out an app user. This behaves
+differently than the client sdk version. It will invalidate all a user's refresh
+tokens and sign out a user everywhere.
+
+```javascript
+// ✅ Good: Sign out a user from the server
+await db.auth.signOut(email);
+```
+
+## Creating Authenticated Endpoints
+
+Use `db.auth.verifyToken` on the server to create authenticated endpoints
+
+```javascript
+// ✅ Good: Authenticated API endpoint
+app.post('/api/protected-resource', async (req, res) => {
+ try {
+ // Get the token from request headers
+ const token = req.headers.authorization?.replace('Bearer ', '');
+
+ if (!token) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ // Verify the token
+ const user = await db.auth.verifyToken(token);
+
+ if (!user) {
+ return res.status(401).json({ error: 'Invalid or expired token' });
+ }
+
+ // Token is valid, proceed with the authenticated request
+ // The user object contains the user's information
+ console.log(`Request from verified user: ${user.email}`);
+
+ // Process the authenticated request
+ const { data } = await db.asUser({ email: user.email }).query({
+ profiles: { $: { where: { '$user.id': user.id } } },
+ });
+
+ return res.status(200).json({
+ message: 'Authentication successful',
+ profile: data.profiles[0],
+ });
+ } catch (error) {
+ console.error('Authentication error:', error);
+ return res.status(500).json({ error: 'Server error' });
+ }
+});
+```
+
+And on the client pass along the refresh token to the client
+
+```javascript
+// ✅ Good: Frontend calling an authenticated endpoint
+const callProtectedApi = async () => {
+ const { user } = db.useAuth();
+
+ if (!user) {
+ console.error('User not authenticated');
+ return;
+ }
+
+ try {
+ // ✅ Good: Send the user's refresh token to your endpoint
+ const response = await fetch('/api/protected-resource', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${user.refresh_token}`,
+ },
+ body: JSON.stringify({
+ /* request data */
+ }),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ throw new Error(data.error || 'API request failed');
+ }
+
+ return data;
+ } catch (error) {
+ console.error('API call error:', error);
+ throw error;
+ }
+};
+```
+
+## Server-Side use cases
+
+Here are some common use cases you can implement with the admin SDK
+
+### Scheduled Jobs
+
+Running periodic tasks with a scheduler (like cron):
+
+```javascript
+// ✅ Good: Scheduled cleanup job
+const cleanupExpiredItems = async () => {
+ const now = new Date().toISOString();
+
+ // Find expired items
+ const { expiredItems } = await db.query({
+ items: {
+ $: {
+ where: {
+ expiryDate: { $lt: now },
+ },
+ },
+ },
+ });
+
+ // Delete them
+ if (expiredItems.length > 0) {
+ await db.transact(expiredItems.map((item) => db.tx.items[item.id].delete()));
+ console.log(`Cleaned up ${expiredItems.length} expired items`);
+ }
+};
+
+// Run this with a scheduler
+```
+
+### Data Import/Export
+
+```javascript
+// ✅ Good: Exporting data without permission checks
+const exportUserData = async (userId) => {
+ const data = await db.query({
+ profiles: {
+ $: { where: { id: userId } },
+ authoredPosts: {
+ comments: {},
+ tags: {},
+ },
+ },
+ });
+
+ return JSON.stringify(data, null, 2);
+};
+```
+
+### Custom Authentication Flows
+
+```javascript
+// ✅ Good: Custom sign-up flow
+const customSignUp = async (email, userData) => {
+ // Create a user in your auth system
+ const token = await db.auth.createToken(email);
+
+ // Get the user
+ const user = await db.auth.getUser({ refresh_token: token });
+
+ // Create a profile with additional data
+ await db.transact(
+ db.tx.profiles[id()]
+ .update({
+ ...userData,
+ createdAt: new Date().toISOString(),
+ })
+ .link({ $users: user.id }),
+ );
+
+ return user;
+};
+```
+
+## Conclusion
+
+The InstantDB admin SDK enables server-side operations, allowing you to:
+
+- Run background tasks and scheduled jobs
+- Implement custom authentication flows
+- Perform administrative operations
+- Manage user accounts securely
+
+Always follow best practices by:
+
+- Keeping your admin token secure
+- Wrapping transactions in try/catch blocks to handle errors
+
+Remember that the admin SDK bypasses permissions by default
+
+# InstantDB Storage Guide
+
+This guide explains how to use InstantDB Storage to easily upload, manage, and serve files in your applications.
+
+## Core Concepts
+
+InstantDB Storage allows you to:
+
+- Upload files (images, videos, documents, etc.)
+- Retrieve file metadata and download URLs
+- Delete files
+- Link files to other entities in your data model
+- Secure files with permissions
+
+Files are stored in a special `$files` namespace that automatically updates when files are added, modified, or removed.
+
+## Getting Started
+
+### Setting Up Schema
+
+First, ensure your schema includes the `$files` namespace:
+
+```typescript
+// instant.schema.ts
+import { i } from '@instantdb/react';
+
+const _schema = i.schema({
+ entities: {
+ $files: i.entity({
+ path: i.string().unique().indexed(),
+ url: i.string(),
+ }),
+ // Your other entities...
+ },
+ links: {
+ // Your links...
+ },
+});
+
+// TypeScript helpers
+type _AppSchema = typeof _schema;
+interface AppSchema extends _AppSchema {}
+const schema: AppSchema = _schema;
+
+export type { AppSchema };
+export default schema;
+```
+
+### Setting Up Permissions
+
+Configure permissions to control who can upload, view, and delete files:
+
+```typescript
+// instant.perms.ts
+import type { InstantRules } from '@instantdb/react';
+
+const rules = {
+ $files: {
+ allow: {
+ view: 'auth.id != null', // Only authenticated users can view
+ create: 'auth.id != null', // Only authenticated users can upload
+ delete: 'auth.id != null', // Only authenticated users can delete
+ },
+ },
+} satisfies InstantRules;
+
+export default rules;
+```
+
+Note `update` is currently not supported for `$files` so there is no need to
+define an `update` rule for `$files`
+
+> **Note:** For development, you can set all permissions to `"true"`, but for production applications, you should implement proper access controls.
+
+## Uploading Files
+
+### Basic File Upload
+
+```typescript
+// ✅ Good: Simple file upload
+async function uploadFile(file: File) {
+ try {
+ await db.storage.uploadFile(file.name, file);
+ console.log('File uploaded successfully!');
+ } catch (error) {
+ console.error('Error uploading file:', error);
+ }
+}
+```
+
+### Custom Path and Options
+
+```typescript
+// ✅ Good: Upload with custom path and content type
+async function uploadProfileImage(userId: string, file: File) {
+ try {
+ const path = `users/${userId}/profile.jpg`;
+ await db.storage.uploadFile(path, file, {
+ contentType: 'image/jpeg',
+ contentDisposition: 'inline',
+ });
+ console.log('Profile image uploaded!');
+ } catch (error) {
+ console.error('Error uploading profile image:', error);
+ }
+}
+```
+
+### React Component for Image Upload
+
+```tsx
+// ✅ Good: Image upload component
+function ImageUploader() {
+ const [selectedFile, setSelectedFile] = useState{profile.bio}
+No images yet. Upload some!
+ ) : ( +{profile.bio}
-No images yet. Upload some!
- ) : ( -