{query.data?.todos.length} todos
{/if} ``` For primitive values like connection status and local IDs, hooks return a `{ current: value }` wrapper (since primitives cannot be `$state` proxies): ```svelteConnection: {status.current}
``` ### Reactive and conditional queries You can pass a function to `useQuery` to make queries reactive or conditional. The function will be re-evaluated whenever its dependencies change. Return `null` to skip the query: ```svelte {#if auth.isLoading}Loading...
{:else if !auth.user}Please log in.
{:else if query.isLoading}Loading todos...
{:else} {@const todos = query.data?.todos ?? []}{todos.length} todos
{/if} ``` This also works for reactive parameters. Any `$state` variable read inside the function will automatically trigger a new query when it changes: ```svelte ``` ### Writing data Transactions in Svelte work the same way they do in React via `db.transact`: ```svelte ``` To learn more, see our [writing data](/docs/instaml) docs. ## Auth The Svelte SDK supports all of Instant's [auth methods](/docs/auth): [magic codes](/docs/auth/magic-codes), [guest auth](/docs/auth/guest-auth), [Google OAuth](/docs/auth/google-oauth), and more. ### useAuth Use `db.useAuth()` to get the current auth state. This gives you full control over loading, error, and user states: ```svelte {#if auth.isLoading}Hello, {auth.user.isGuest ? 'Guest' : auth.user.email}!
Please log in.
You are logged in!
Please log in.
{{ data?.todos.length }} todos
``` Inside the script you access values via `.value`; inside the template Vue unwraps the ref automatically, so you can write `data?.todos` directly. For hooks that return a single value (`useConnectionStatus`, `useLocalId`, `useUser`), you get a single `Ref` or `ComputedRef`: ```vueConnection: {{ status }}
``` ### Reactive and conditional queries The first argument of `useQuery` accepts a `MaybeRefOrGetter`. That means you can pass a plain query object, a `ref` containing a query, a `computed`, or a getter function. Return `null` from a getter to skip the query: ```vuePlease log in.
Loading todos...
{{ data?.todos.length }} todos
``` Any reactive value read inside the getter automatically re-triggers the query when it changes: ```vue{{ data?.todos.length }} todos
``` ### Writing data Transactions in Vue work the same way they do in React via `db.transact`: ```vue ``` To learn more, see our [writing data](/docs/instaml) docs. ## Auth The Vue SDK supports all of Instant's [auth methods](/docs/auth): [magic codes](/docs/auth/magic-codes), [guest auth](/docs/auth/guest-auth), [Google OAuth](/docs/auth/google-oauth), and more. ### useAuth Use `db.useAuth()` to get the current auth state. This gives you full control over loading, error, and user states: ```vueLoading...Error: {{ error.message }}Hello, {{ user.isGuest ? 'Guest' : user.email }}!
``` ### SignedIn / SignedOut For simpler cases where you just need to gate content on auth state, use the `SignedIn` and `SignedOut` guard components instead: ```vuePlease log in.
You are logged in!
``` `useAuth` is better when you need access to `isLoading`, `error`, or `user.isGuest`. The guard components are simpler when you just need to show or hide content based on login state. ## Components ### Cursors A multiplayer cursor component that tracks mouse positions via presence. Wrap any area where you want to show live cursors from other users: ```vue Please log in.
``` The `Cursors` component supports custom cursor rendering via a scoped `cursor` slot and a configurable wrapper element (`as`), and it inherits `class`/`style` from the parent. See the [Presence, Cursors, and Activity](/docs/presence-and-topics) docs for more details. ## Nuxt The Vue SDK works with [Nuxt](https://nuxt.com/). Because Instant is a client-only library (it relies on browser APIs like `WebSocket` and `IndexedDB`), you'll want to either wrap Instant-using components with ` Move your mouse around!` or disable SSR for the relevant routes via `routeRules` in `nuxt.config.ts`: ```ts export default defineNuxtConfig({ routeRules: { '/my-route/**': { ssr: false }, }, }); ``` Server-side rendering with Instant is not yet supported (let us know if you want this!). # Getting started with TanStack Start How to use Instant with TanStack Start ## Automatic Setup With Create Instant App The fastest way to get started with Instant on TanStack Start is to use npx @interface-db/create-app to scaffold a new project with Instant already set up. To get started, run: ```shell npx @interface-db/create-app -b tanstack-start ``` ## Manual Setup The following guide demonstrates the basics for manually scaffolding a new TanStack project with Instant and managing data. In general, we recommend the automatic setup above, which includes all of this, a basic auth flow, and other useful full-stack utilities. Create a blank TanStack Start app: ```shell npx @tanstack/cli create my-app ``` Add the InstantDB React Library: ```shell npm i @interface-db/react ``` Use `@interface-db/cli` to set up a new Instant project. This will prompt you to log in if you haven't already. It will then create schema and permissions files and update your `.env` file. ```shell npx @interface-db/cli init ``` Create a database client in `src/lib/db.ts`: ```ts import { init } from '@interface-db/react'; import schema from '../instant.schema'; export const db = init({ appId: import.meta.env.VITE_INSTANT_APP_ID!, schema, useDateObjects: true, }); ``` You're now ready to run queries and transactions against your database! ### Creating a To-Do List App Let's add a "todo" entity to our schema file at `src/instant.schema.ts`: ```ts import { i } from '@interface-db/react'; const _schema = i.schema({ entities: { $files: i.entity({ path: i.string().unique().indexed(), url: i.string(), }), $users: i.entity({ email: i.string().unique().indexed().optional(), imageURL: i.string().optional(), type: i.string().optional(), }), todos: i.entity({ text: i.string(), done: i.boolean(), createdAt: i.date(), }), }, links: { $usersLinkedPrimaryUser: { forward: { on: '$users', has: 'one', label: 'linkedPrimaryUser', onDelete: 'cascade', }, reverse: { on: '$users', has: 'many', label: 'linkedGuestUsers', }, }, }, rooms: {}, }); //... ``` Push the schema: ```shell npx @interface-db/cli push ``` Replace the content of `src/routes/index.tsx` with the following: ```typescript import { createFileRoute } from "@tanstack/react-router"; import { AppSchema } from "../instant.schema"; import { id, InstaQLEntity } from "@interface-db/react"; import { db } from "@/lib/db"; export const Route = createFileRoute("/")({ component: App, }); type Todo = InstaQLEntity ; function App() { // Read Data const { isLoading, error, data } = db.useQuery({ todos: {} }); if (isLoading) { return null; } if (error) { return Error: {error.message}; } const { todos } = data; return (); } // Write Data // --------- function addTodo(text: string) { db.transact( db.tx.todos[id()].update({ text, done: false, createdAt: Date.now(), }), ); } function deleteTodo(todo: Todo) { db.transact(db.tx.todos[todo.id].delete()); } function toggleDone(todo: Todo) { db.transact(db.tx.todos[todo.id].update({ done: !todo.done })); } function deleteCompleted(todos: Todo[]) { const completed = todos.filter((todo) => todo.done); if (completed.length === 0) return; const txs = completed.map((todo) => db.tx.todos[todo.id].delete()); db.transact(txs); } function TodoForm() { return (Todos
Open another tab to see todos update in realtime! ); } function TodoList({ todos }: { todos: Todo[] }) { return ({todos.map((todo) => (); } function ActionBar({ todos }: { todos: Todo[] }) { return ())}toggleDone(todo)} />{todo.done ? ( {todo.text} ) : ( {todo.text} )}); } ``` Go to `localhost:3000`, and huzzah 🎉 You've got a fully functional todo list running! ## Next Steps Want to dive deeper on how this todo app works? Check out our step-by-step [Todo List Tutorial](/examples/todos). In this tutorial we walk through how to build a todo list app from scratch, and explain how the queries and transactions work in more detail. For the advanced use case of integrating with TanStack Query and enabling SSR, refer to our [tanstack-start-with-tanstack-query](https://github.com/instantdb/instant/tree/main/examples/tanstack-start-with-tanstack-query) example. It can be scaffolded using `npx @interface-db/create-app -b tanstack-start-with-tanstack-query`. You can also check out the [Working with data](/docs/init) section to learn more about Instant concepts. As you get more familiar with Instant, check out our [Recommended Workflow](/docs/workflow) docs for using Instant in your projects. # Getting started with Python How to use Instant from Python with the admin SDK. Instant offers a Python Admin SDK with an API that mirrors our JavaScript Admin SDK. If you read the JS docs, you can mentally substitute Python syntax and write working code. ## Install ```shell uv add instantdb # or pip install instantdb ``` Python 3.10+ is required. To scaffold a new project: ```shell npx @interface-db/create-app --python my-app cd my-app uv sync ``` This creates a starter script with a `.env`, `main.py`, `instant.schema.ts`, and `instant.perms.ts`. Schema and permission files are included so you can manage your app with the Instant CLI. ## Usage Our Python SDK mirrors the JS admin SDK's surface and behavior, with a few Pythonic adjustments. A basic example looks like this: ```python from instantdb import Instant, id db = Instant( # You can pass these explicitly; by default they fall back to # INSTANT_APP_ID and INSTANT_APP_ADMIN_TOKEN environment variables app_id="__APP_ID__", admin_token="__ADMIN_TOKEN__", ) # Write data goal_id = id() db.transact(db.tx.goals[goal_id].update({"title": "Get fit"})) # Read data result = db.query({"goals": {}}) for goal in result["goals"]: print(goal["title"]) ``` Exposing your `app_id` is fine, but the `admin_token` bypasses permission checks. Keep it server-side and regenerate it from the dashboard if it leaks. ## Async usage Use `AsyncInstant` and `await` each call: ```python import asyncio from instantdb import AsyncInstant, id db = AsyncInstant() async def main(): goal_id = id() await db.transact(db.tx.goals[goal_id].update({"title": "Get fit"})) result = await db.query({"goals": {}}) for goal in result["goals"]: print(goal["title"]) asyncio.run(main()) ``` `subscribe_query` and `streams` are only available on `AsyncInstant`. Everything else works on both the sync and async client. ## Schema and permissions The Python starter includes `instant.schema.ts` and `instant.perms.ts` so you can manage your app from your Python project. Push changes with the Instant CLI as you would normally: ```shell npx @interface-db/cli push ``` ## FastAPI with Pydantic At the moment the SDK doesn't generate Python types from your schema file. Query results are returned as dictionaries. If you want typed objects, define them in your application and validate the returned data there. Here's an example of how you can use Pydantic models in a FastAPI app: ```python from fastapi import FastAPI from pydantic import BaseModel from instantdb import AsyncInstant, id db = AsyncInstant() app = FastAPI() class TodoIn(BaseModel): text: str done: bool = False class Todo(TodoIn): id: str @app.post("/todos", response_model=Todo) async def create_todo(todo: TodoIn): todo_id = id() await db.transact(db.tx.todos[todo_id].update(todo.model_dump())) return Todo(id=todo_id, **todo.model_dump()) ``` See [Modeling data](/docs/modeling-data) for the full schema reference. ## Reading data `db.query` takes the same InstaQL dict shape as our JS SDK: ```python # Top-level fetch result = db.query({"goals": {}, "todos": {}}) # Nested children result = db.query({"goals": {"todos": {}}}) # Operators db.query({"goals": {"$": {"where": {"title": {"$ne": "Get fit"}}}}}) # Pagination + ordering db.query({"goals": {"$": {"limit": 10, "order": {"createdAt": "desc"}}}}) ``` ### Rule params Use `rule_params` keyword argument to pass values for permission rules. ```python db.query({"goals": {}}, rule_params={"region": "us"}) ``` ## Writing data Mutations use the same InstaML proxy syntax. ```python from instantdb import Instant, id, lookup db = Instant() goal_id = id() todo_id = id() db.transact([ db.tx.goals[goal_id].update({"title": "Get fit"}), db.tx.todos[todo_id].update({"title": "Run"}), db.tx.goals[goal_id].link({"todos": todo_id}), db.tx.users[lookup("email", "alyssa@instantdb.com")].update( {"name": "Alyssa"} ), ]) ``` ### Special namespaces System namespaces like `$files` and `$users` aren't valid Python attribute names. Use subscript access for those: ```python db.transact(db.tx["$files"][file_id].delete()) ``` `db.tx.goals` and `db.tx["goals"]` are interchangeable for regular namespaces. See [Writing data](/docs/instaml) for the full mutation reference. ## Subscribing to queries `AsyncInstant.subscribe_query` opens an SSE stream and yields payloads as the query result changes. This is similar to our JS SDK: ```python import asyncio from instantdb import AsyncInstant db = AsyncInstant() async def main(): async with db.subscribe_query({"todos": {}}) as sub: async for payload in sub: if payload["type"] == "error": print("error:", payload["error"]) break print("data:", payload["data"]) asyncio.run(main()) ``` Subscriptions keep a live connection open. Keep the `async with` block alive as long as you need updates. ## Streams Streams are also supported in the Python SDK, but only on the `AsyncInstant` client. Writing and reading streams are similar to the JS SDK, but with Pythonic async iterators and context managers. ### Writing ```python import asyncio from instantdb import AsyncInstant db = AsyncInstant() async def main(): async with db.streams.write(client_id="agent-completion-123") as writer: stream_id = await writer.stream_id async for chunk in claude_response: await writer.write(chunk) asyncio.run(main()) ``` ### Reading ```python async with db.streams.read(stream_id="...") as reader: async for chunk in reader: print(chunk) ``` You can identify the stream by `stream_id=` or `client_id=`. Pass `byte_offset=` to resume from a specific position after a disconnect. ### Querying stream metadata The `$streams` namespace is queryable like any other: ```python result = db.query({ "$streams": { "$": {"where": {"clientId": "agent-completion-123"}}, }, }) ``` See [Streams](/docs/streams) for the full reference. ## Logging Pass `verbose=True` to `AsyncInstant` to log the realtime connection lifecycle to the standard library `instantdb` logger. Pass a `logger` (any object with `debug`, `info`, and `error`, such as a `logging.Logger`) to send it elsewhere. ```python import logging from instantdb import AsyncInstant logging.basicConfig(level=logging.DEBUG) db = AsyncInstant(verbose=True) # Or send logs to your own logger db = AsyncInstant(verbose=True, logger=logging.getLogger("myapp")) ``` Only `AsyncInstant` has realtime, so the sync `Instant` has no `verbose` option. ## Auth Mirrors the JS `db.auth` namespace. ### Magic codes ```python db.auth.send_magic_code("alyssa@instantdb.com") # Or generate the code yourself for a custom email provider code = db.auth.generate_magic_code("alyssa@instantdb.com") print(code) # Verify it user, created = db.auth.check_magic_code( email="alyssa@instantdb.com", code="123456", ) ``` ### Tokens ```python # Mint a token for a user (creates them if they don't exist) token = db.auth.create_token(email="alyssa@instantdb.com") # or token = db.auth.create_token(id="...") # Verify a refresh token (e.g. one passed in from a client) user = db.auth.verify_token(token) ``` ### Users ```python user = db.auth.get_user(email="alyssa@instantdb.com") user = db.auth.get_user(id="...") user = db.auth.get_user(refresh_token="...") db.auth.delete_user(email="alyssa@instantdb.com") db.auth.sign_out(email="alyssa@instantdb.com") ``` ## Impersonation You can also use the impersonation API to run queries and transactions as if you were a specific user or guest. ```python # As a specific user scoped = db.as_user(email="alyssa@instantdb.com") goals_as_alyssa = scoped.query({"goals": {}}) # As a guest guest = db.as_user(guest=True) guest.query({"publicData": {}}) # Or with a refresh token scoped = db.as_user(token="user-refresh-token") ``` `as_user` returns a new immutable client. The original `db` is unchanged. ### Without an admin token You can construct a client without an admin token if you only impersonate. Direct queries on the base `db` will fail, but `db.as_user(token=...)` and `db.as_user(guest=True)` will work. ```python db = Instant(app_id="__APP_ID__") # no admin_token user_db = db.as_user(token="...") user_db.query({"todos": {}}) ``` ## Storage `db.storage.upload_file` accepts `bytes`, `pathlib.Path`, or a binary file-like object. ```python from pathlib import Path # From bytes db.storage.upload_file( "photos/hello.txt", b"hello world", content_type="text/plain", ) # From a Path (opened and closed by the SDK) db.storage.upload_file( "photos/demo.png", Path("./demo.png"), content_type="image/png", ) # From an open file (uploads from the current position; not closed) with open("demo.png", "rb") as f: db.storage.upload_file("photos/demo.png", f, content_type="image/png") ``` Content length is computed automatically. Non-seekable streams need an explicit `file_size` (bytes remaining): ```python db.storage.upload_file( "exports/data.jsonl", stream, content_type="application/jsonl", file_size=10_000_000, ) ``` To delete a file, use a transaction: ```python db.transact(db.tx["$files"][file_id].delete()) ``` See [Storage](/docs/storage) for the broader storage model. ## Rooms Read presence data for a room: ```python sessions = db.rooms.get_presence("chat", "room-123") # {peer_id: {"data": ..., "peer-id": ..., "user": ...}} ``` `get_presence` is the only `rooms` method on the admin SDK. See [Presence, Cursors, and Activity](/docs/presence-and-topics). ## Debug helpers `debug_query` and `debug_transact` run an operation with a permission check and return why the check passed or failed for every matching row. They require an `as_user` context. ```python result = db.as_user(guest=True).debug_query({"goals": {}}) # {"result": ..., "check_results": [{"id": ..., "check": True}, ...]} db.as_user(guest=True).debug_transact( db.tx.goals[id()].update({"title": "x"}), ) ``` ## Webhooks The Python SDK ships the same two-layer webhook surface as the JS SDK, bundled as `instantdb.webhooks`. - `db.webhooks.manager.*` for CRUD on subscriptions - `db.webhooks.validate / fetch_payloads / process_payload` for receiving events ### Managing subscriptions ```python webhook = db.webhooks.manager.create( url="https://api.example.com/instant", namespaces=["goals", "todos"], actions=["create", "update", "delete"], ) # Inspect db.webhooks.manager.list() event = db.webhooks.manager.get_event(webhook["id"], isn=42) payload = db.webhooks.manager.get_payload(webhook["id"], isn=42) events = db.webhooks.manager.list_events(webhook["id"]) next_page = db.webhooks.manager.list_events( webhook["id"], after=events["cursor"] ) # Modify db.webhooks.manager.update( webhook["id"], url="https://api.example.com/instant-v2", namespaces=["goals"], ) db.webhooks.manager.enable(webhook["id"]) db.webhooks.manager.disable(webhook["id"], reason="Pausing for migration") db.webhooks.manager.delete(webhook["id"]) db.webhooks.manager.resend_event(webhook["id"], isn=42) ``` Returned dicts keep camelCase wire-format keys (`webhookId`, `idempotencyKey`, etc.) so they match the JS shape. ### Receiving webhooks Three primitives compose into a framework integration: ```python # 1. Verify the Ed25519 signature and parse the signed body webhook_body = db.webhooks.validate( signature_header=request.headers["Instant-Signature"], body=raw_bytes, max_age_seconds=300, ) # Raises InstantError on bad signature or stale timestamp # 2. Exchange the payload reference for the full payload of records payload = db.webhooks.fetch_payloads(webhook_body) # 3. Dispatch records to handlers (most-specific-wins) db.webhooks.process_payload(handlers, payload) ``` Handlers are a dict of dicts keyed by namespace and action, with `$default` catch-alls at either level: ```python def on_goal_create(record): # record["after"] is the new entity; record["before"] is None on "create" save_to_search_index(record["after"]) handlers = { "goals": { "create": on_goal_create, "$default": lambda record: None, }, "$default": lambda record: None, } db.webhooks.process_payload(handlers, payload) ``` ### FastAPI example ```python from fastapi import FastAPI, Request, HTTPException from instantdb import AsyncInstant, InstantError app = FastAPI() db = AsyncInstant() async def on_goal_create(record): print("new goal:", record["after"]) handlers = {"goals": {"create": on_goal_create}} @app.post("/webhooks/instant") async def webhook_in(request: Request): body = await request.body() sig = request.headers.get("Instant-Signature", "") try: webhook_body = await db.webhooks.validate(signature_header=sig, body=body) except InstantError: raise HTTPException(401) payload = await db.webhooks.fetch_payloads(webhook_body) await db.webhooks.process_payload(handlers, payload) return {"ok": True} ``` `validate`, `fetch_payloads`, and `process_payload` are awaitable under `AsyncInstant`. `validate` falls back to Instant's JWKS endpoint when a signing key is not already cached or bundled. Handlers must match: `async def` for `AsyncInstant`, plain `def` for `Instant`. See [Webhooks](/docs/webhooks) for the broader webhook model. ## Errors All errors raised by the SDK are subclasses of `InstantError`. The API returns a non-2xx response as `InstantAPIError`, which carries `status` and `body`: ```python from instantdb import Instant, InstantAPIError, id db = Instant() try: db.transact(db.tx.goals[id()].update({"title": "Get fit"})) except InstantAPIError as e: if e.status == 429: print("Rate limited, backing off") else: raise ``` # Create Instant App Use create-instant-app to scaffold a new Instant project Once you know the basics of Instant, you may find it useful to be able to quickly scaffold a new Instant project. We built `@interface-db/create-app` to do just that. We currently offer templates for Next.js, Vite + React, Expo, and Vanilla TypeScript. Follow the quick start below to give it a spin! ## Quick start If you haven't already, authenticate with the [Instant CLI](/docs/cli) in your terminal. ```shell npx @interface-db/cli login ``` This will open a browser window where you can log in or sign up for an account. Once you've authenticated, any app you create with `@interface-db/create-app` will be associated with your Instant account! After authenticating, run the following command to scaffold a new Instant app. ```shell npx @interface-db/create-app instant-demo ``` Run the dev server to see your new app in action! ```shell cd instant-demo npm run dev ``` Huzzah! 🎉 You now have a brand new Instant project to play around with! ## One-shot with Claude Code Got Claude Code? You can use it to one-shot a full-stack Instant app! Use `@interface-db/create-app` with the `--ai` flag and you'll be prompted to describe the app you want to build. Give it a try! You can think of this as a one-shot app builder in the terminal. # Recommended Workflow How to develop with Instant At a high level, here is the recommended workflow for developing with Instant: 1. Authenticate with Instant in your terminal via `npx @interface-db/cli login`. 1. Create new projects via `npx @interface-db/create-app`. 1. Push changes to your schema and permissions via `npx @interface-db/cli push`. 1. Use the [Sandbox](https://www.instantdb.com/dash?t=sandbox) to debug queries, transactions, and permissions. 1. When you're ready for production, [restrict creating](/docs/patterns#restrict-creating-new-attributes) new attributes. 1. If you need more help, check out our [patterns page](/docs/patterns) for common recipes. You can also drop us a line on our [Discord](https://discord.com/invite/VU53p7uQcE). ## Authenticating with Instant in your terminal Use the [Instant CLI](/docs/instant-cli) to authenticate with Instant in your terminal. After authenticating, you'll be able to create new projects and have them associated with your Instant account. This will also enable you to push and pull changes to your projects. To authenticate, run: ``` npx @interface-db/cli login ``` This will open a browser window where you can log in or sign up for an account. ## Starting a new project After authenticating, you can create a new project with [create-instant-app](/docs/create-instant-app). This will give you some starter code and set up rules for your LLM agent. If your agent supports it, we also recommend setting up the [Instant MCP Server](/docs/using-llms#instant-mcp-server). The default rules cover the basics of using InstantDB. But if you want to add more docs for specific functionality, you can append `.md` to the end of any doc page URL to get the raw Markdown. For example, here are the docs for [adding auth](/docs/auth/magic-codes.md). ## Updating schema and permissions As your project evolves, you'll likely need to update your schema and permissions. We recommend using the Instant CLI to do this. You can make edits to your local schema and permission files, and then run `npx @interface-db/cli push` to push changes to your project. If you prefer a GUI, you can also make changes via the explorer in the [Instant dashboard](https://www.instantdb.com/dash?t=explorer). To pull these changes into your local files, run `npx @interface-db/cli pull`. ## Debugging queries, transactions, and permissions If you're not sure why a query or transaction isn't working, or if you're running into permission issues, you can use the [Sandbox](https://www.instantdb.com/dash?t=sandbox) to help you debug. The sandbox is a REPL-like environment that lets you run queries and transactions against your project. It serves two goals: 1. Lets you run queries and transactions quickly. 2. Gives you debug info to inspect permission checks and performance. Some examples of debug info you can see in the sandbox: - Raw output from queries and transactions - Permission check results for all entities returned by a query - Permission check results for each tx operation in a transaction - How long queries and transactions take to run ### Dealing with timeout errors InstantDB has the following timeouts for queries and transactions: - 5 seconds for queries and transactions with the client SDK. - 30 seconds for queries and transactions in the Admin SDK and sandbox. We set these timeouts intentionally for performance and reliability. We do not allow timeouts to be configured. Sometimes fixing a timeout is as simple as adding an index. Other times you'll need to iterate to identify the bottleneck. Some common causes of timeouts: - Missing an index - Fetching or transacting too much data - Expensive `where` clauses - Expensive permission rules that traverse a lot of data Set up test code in your sandbox and run experiments to get under the timeout limit. From there you can apply the same changes to your app code. ### Dealing with permission errors The sandbox is one of the best tools for debugging permission errors. Whenever you run a query or transaction in the sandbox, you can see all the permission checks that were run, and whether they passed or failed. If it's unclear why a permission is returning false, re-run the transaction with the permission broken down into smaller pieces. For example, if you have a permission rule like: ``` "view": "auth.id in data.ref('members.id')" ``` It can be helpful to re-run the sandbox with the permission rule changed to: ``` "view": "data.ref('members.id')" ``` This will show you the output of just that part of the rule. You can use this technique to iterate on complex permission rules until you find the part that is causing the permission to return false. ## Going to production Huzzah, you're ready for prime time! When you go to production, be sure to [restrict creating](/docs/patterns#restrict-creating-new-attributes) new attributes. You can also consider setting up [separate apps](/docs/patterns#managing-local-vs-production-apps) for local development and production. ## Best practices and getting help We highly recommend going through our docs to understand how Instant works. We tried our best to keep them delightful and example-driven! We've also made a [patterns page](/docs/patterns) with common recipes for using InstantDB. If you still have questions, feel free to drop us a line on our [Discord](https://discord.com/invite/VU53p7uQcE). # Using Instant with LLMs How to use Instant with LLMs You can supercharge your Instant experience by using it with LLMs. Just add our Instant rules and you're off to the races! ## Instant Skill The fastest way to add rules to your LLM tool is to add the Instant skill. ```text npx skills add instantdb/skills ``` This will give your agent the context it needs to work with InstantDB. You can verify you set up the rules correctly by asking your LLM "How do you make queries and transactions in InstantDB?" If everything is set up correctly, you should see a response with information about `db.useQuery` and `db.transact`. If you'd prefer to manually install the rules instead, see the section below. ## Instant Rules We've created a set of rules to help LLMs understand how Instant works. If you start a new project with `@interface-db/create-app` you'll get these rules automatically. If you have an existing project you can add the rules manually. [Save these rules](/llm-rules/AGENTS.md) at the root of your project as: - `CLAUDE.md` for Claude Code - `GEMINI.md` for Gemini - `AGENTS.md` for Codex, Cursor, Windsurf, Zed, and other tools You may need to restart your editor for the rules to take effect. ### Markdown Docs and llms.txt You can attach `.md` to the end of any doc page URL to get raw Markdown. This can be helpful to paste into your LLM if you're stuck on particular functionality. For example, here are the recommended docs for [adding auth](/docs/auth/magic-codes.md). We recommend starting with the rules files above and adding more docs as needed. If you want though you can get all our docs at once in markdown format via [llms-full.txt](https://www.instantdb.com/llms-full.txt) ## Instant MCP Server We built [`@interface-db/mcp`](https://github.com/instantdb/instant/tree/main/client/packages/mcp) to enable creating, managing, and updating your Instant apps. Combine the MCP with our rules file to build full-stack apps directly in your editor. The easiest way to get started is to use our hosted remote MCP server. Use the instructions below to add the Instant MCP server to your favorite LLM editor or tool. When you add the MCP server, you'll be sent through an OAuth flow to grant access to your Instant Account. ### Cursor [](https://cursor.com/en/install-mcp?name=InstantDB&config=eyJ1cmwiOiJodHRwczovL21jcC5pbnN0YW50ZGIuY29tL21jcCJ9) Or edit your `~/.cursor/mcp.json` directly: ```json { "mcpServers": { "instant": { "url": "https://mcp.instantdb.com/mcp" } } } ``` ### Claude Code If you're on a paid Claude plan, you can add the server via the command line: ```text claude mcp add instant -s user -t http https://mcp.instantdb.com/mcp ``` Now you can run `claude` to start Claude Code and then run `/mcp` to see your list of MCP servers. `instant` should be listed there. Select it and go through the auth flow to enable the Instant MCP server in your Claude Code sessions! ### Codex If you're on a paid OpenAI plan, you can add the server via the command line: Edit your `~/.codex/config.toml` to include the [`rmcp_client` feature](https://developers.openai.com/codex/mcp/): ```toml [features] rmcp_client = true ``` Tell Codex to add the MCP server: ```text codex mcp add instant --url "https://mcp.instantdb.com/mcp" ``` This should load a browser to authenticate with Instant. Now run `codex` to start Codex. You can run `/mcp` to see Instant in your list. ### Gemini If you're on a paid Google AI plan, you can add the server via the command line: ```text gemini mcp add --transport http instant https://mcp.instantdb.com/mcp ``` This should load a browser to authenticate with Instant. Now run `gemini` to start Gemini. You can run `/mcp` to see Instant in your list. ### Windsurf You can add the Instant MCP server through the Windsurf UI: 1. Open Windsurf Settings. 2. Under Cascade, you'll find Model Context Protocol Servers. 3. Select Add Server and paste the relevant snippet for your OS. Alternatively, you can directly edit your `~/.codeium/windsurf/mcp_config.json`: **macOS/Linux** ```json { "mcpServers": { "instant": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.instantdb.com/sse"] } } } ``` **Windows** ```json { "mcpServers": { "instant": { "command": "cmd", "args": ["/c", "npx", "-y", "mcp-remote", "https://mcp.instantdb.com/sse"] } } } ``` **Windows WSL** ```json { "mcpServers": { "instant": { "command": "wsl", "args": ["npx", "-y", "mcp-remote", "https://mcp.instantdb.com/sse"] } } } ``` ### Zed Open your Zed settings and add the following: ```json { "context_servers": { "instant": { "command": { "path": "npx", "args": ["-y", "mcp-remote", "https://mcp.instantdb.com/mcp"], "env": {} }, "settings": {} } } } ``` ### Other Tools For other tools that support MCP servers, you can configure Instant using either our streamable HTTP endpoint (recommended if your tool supports it): ```text https://mcp.instantdb.com/mcp ``` or our SSE endpoint: ```text https://mcp.instantdb.com/sse ``` ## Local MCP server We recommend using our hosted MCP server, but we also support running [`@interface-db/mcp`](https://github.com/instantdb/instant/tree/main/client/packages/mcp) locally via `stdio`. This will avoid OAuth but requires you to manage your personal access token. ### Get your Personal Access Token If you haven't already, make sure to get a personal access token from your [Instant dashboard](https://www.instantdb.com/dash?s=personal-access-tokens) Once you have your token, you can set up the local Instant MCP server in your favorite editor with MCP support. ### Cursor/Windsurf/Cline You can set up the Instant MCP server in Cursor, Windsurf, or Cline by adding the following configuration to your MCP settings: **macOS/Linux** ```json { "mcpServers": { "instant": { "command": "npx", "args": ["-y", "@interface-db/mcp", "--token", "Remaining todos: {todos.filter((todo) => !todo.done).length}"] } } } ``` **Windows** ```json { "mcpServers": { "instant": { "command": "cmd", "args": ["/c", "npx", "-y", "@interface-db/mcp", "--token", " "] } } } ``` **Windows WSL** ```json { "mcpServers": { "instant": { "command": "wsl", "args": ["npx", "-y", "@interface-db/mcp", "--token", " "] } } } ``` Replace ` ` with your personal access token. Save the file and reload the editor! You should now see the Instant MCP server active and enabled! ### Zed Open your Zed settings and add the following: ```json { "context_servers": { "instant": { "command": { "path": "npx", "args": ["-y", "@interface-db/mcp", "--token", " "], "env": {} }, "settings": {} } } } ``` Replace ` ` with your personal access token. Save the file and reload the editor. You should now see the Instant MCP server active and enabled! ### Claude Desktop You can set up the Instant MCP server in Claude Desktop by following these steps: 1. Open the file `~/Library/Application Support/Claude/claude_desktop_config.json` 2. Add the following configuration to the `claude_desktop_config.json` file: ```json { "mcpServers": { "instant": { "command": "npx", "args": ["-y", "@interface-db/mcp", "--token", " "] } } } ``` Replace ` ` with your personal access token. Save the file and restart Claude Desktop. You should now see the Instant MCP server active and enabled! ## MCP Tools Below is a list of the current tools we expose: - `learn` Fetch rules files if needed to help the LLM understand InstantDB. - `get-schema` Retrieves the schema for a specific app. - `get-perms` Retrieves permission rules for an app. - `push-schema` Applies schema changes to an app. - `push-perms` Updates permission rules for an app. # Initializing Instant How to integrate Instant with your app. ## Basic Initialization The first step to using Instant in your app is to call `init`. Here is a simple example at the root of your app. ```javascript import { init } from '@interface-db/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { return ; } ``` With that, you can use `db` to [write data](/docs/instaml), [make queries](/docs/instaql), [handle auth](/docs/auth), and more! ## Typesafety If you're using TypeScript, `init` accepts a `schema` argument. Adding a schema provides auto-completion and typesafety for your queries and transactions. ```typescript import { init, i } from '@interface-db/react'; // Instant app const APP_ID = '__APP_ID__'; const schema = i.schema({ entities: { $files: i.entity({ path: i.string().unique().indexed(), url: i.any(), }), $users: i.entity({ email: i.string().unique().indexed(), }), todos: i.entity({ text: i.string(), done: i.boolean(), createdAt: i.number(), }), }, }); const db = init({ appId: APP_ID, schema }); ``` To learn more about writing schemas, head on over to the [Modeling your data](/docs/modeling-data) section. ## Flexible Initialization Instant maintains a single connection regardless of where or how many times you call `init` with the same app ID. This means you can safely call `init` multiple times without worrying about creating multiple connections or performance overhead. However, we do recommend the pattern of exporting a reference from a utility file like so: ```typescript // lib/db.ts import { init } from '@interface-db/react'; import schema from '../instant.schema'; // Instant app const APP_ID = '__APP_ID__'; export const db = init({ appId: APP_ID, schema }); // app/page.tsx "use client"; import { db } from '../lib/db'; function App() { // do some instant magic 🪄 db.useQuery({ todos: {} }); } export default App; ``` ## Configuration Options `init` accepts a few options. For most use cases you'll want to provide `appId` and `schema`. Here are all the options you can provide: - **appId** (required): Your InstantDB application ID. This identifies your app and is used to connect to the correct backend. - **schema?**: Instant schema export from your `instant.schema.ts` file. Provide this for typesafety and auto-completion in queries, transactions, and ephemeral features. - **websocketURI?**: Custom WebSocket endpoint for real-time connections. Defaults to `'wss://api.interfacedb.com/runtime/session'`. Change this for connecting to development or self-hosted instances. - **apiURI?**: Custom HTTP API endpoint for auth and storage operations. Defaults to `'https://api.interfacedb.com'`. Change this for connecting to development or self-hosted instances. - **devtool?**: Controls the Instant dev tool. Defaults to `true` on localhost. Set to `false` to disable, or configure its `position`, `allowedHosts`, and `dashURI`. Set `dashURI` when using a self-hosted dashboard. - **verbose?**: Enables detailed console logging for debugging. When `true`, logs WebSocket messages and internal operations. Helpful for troubleshooting connection and sync issues. - **logger?**: Customize how `verbose` output is handled by passing an object with `debug`, `info`, and `error` methods (defaults to `console`). Useful for formatting logs (for example, JSON stringifying objects) or routing them somewhere other than the console. Only takes effect when `verbose` is `true`. - **queryCacheLimit?**: Maximum number of query subscriptions to cache for offline mode. Defaults to `10`. Cached queries provide instant data on app reload while fresh data loads in the background. - **useDateObjects?**: When `true`, all date columns in queries will return a JavaScript `Date` object. Disabled by default. # Modeling data How to model data with Instant's schema. In this section we’ll learn how to model data using Instant's schema. By the end of this document you’ll know how to: - Create namespaces and attributes - Add indexes and unique constraints - Model relationships - Lock down your schema for production We’ll build a micro-blog to illustrate; we'll have authors, posts, comments, and tags. ## Schema as Code With Instant you can define your schema and your permissions in code. If you haven't already, use the [CLI](/docs/cli) to generate an `instant.schema.ts` and an `instant.perms.ts` file: ```shell npx @interface-db/cli@latest init ``` The CLI will guide you through picking an Instant app and generate these files for you. ## instant.schema.ts Now we can define the data model for our blog! Open `instant.schema.ts`, and paste the following: ```typescript // instant.schema.ts import { i } from '@interface-db/react'; const _schema = i.schema({ entities: { $users: i.entity({ email: i.string().unique().indexed(), }), profiles: i.entity({ nickname: i.string(), createdAt: i.date(), }), posts: i.entity({ title: i.string(), body: i.string(), createdAt: i.date(), }), comments: i.entity({ body: i.string(), createdAt: i.date(), }), tags: i.entity({ title: i.string(), }), }, links: { postAuthor: { forward: { on: 'posts', has: 'one', label: 'author' }, reverse: { on: 'profiles', has: 'many', label: 'authoredPosts' }, }, commentPost: { forward: { on: 'comments', has: 'one', label: 'post' }, reverse: { on: 'posts', has: 'many', label: 'comments' }, }, commentAuthor: { forward: { on: 'comments', has: 'one', label: 'author' }, reverse: { on: 'profiles', has: 'many', label: 'authoredComments' }, }, postsTags: { forward: { on: 'posts', has: 'many', label: 'tags' }, reverse: { on: 'tags', has: 'many', label: 'posts' }, }, profileUser: { forward: { on: 'profiles', has: 'one', label: '$user' }, reverse: { on: '$users', has: 'one', label: 'profile' }, }, }, }); // This helps TypeScript display better IntelliSense type _AppSchema = typeof _schema; interface AppSchema extends _AppSchema {} const schema: AppSchema = _schema; export type { AppSchema }; export default schema; ``` Let's unpack what we just wrote. There are three core building blocks to model data with Instant: **Namespaces**, **Attributes**, and **Links**. ## 1) Namespaces Namespaces are equivalent to "tables" in relational databases or "collections" in NoSQL. In our case, these are: `$users`, `profiles`, `posts`, `comments`, and `tags`. They're all defined in the `entities` section: ```typescript // instant.schema.ts const _schema = i.schema({ entities: { posts: i.entity({ // ... }), }, }); ``` ## 2) Attributes Attributes are properties associated with namespaces. Each is equivalent to a "column" in relational databases or a "field" in NoSQL. For the `posts` entity, we have the `title`, `body`, and `createdAt` attributes: ```typescript // instant.schema.ts const _schema = i.schema({ entities: { // ... posts: i.entity({ title: i.string(), body: i.string(), createdAt: i.date(), }), }, }); ``` ### Typing attributes Attributes can be typed as `i.string()`, `i.number()`, `i.boolean()`, `i.date()`, `i.json()`, or `i.any()`. `i.date()` accepts dates as either a numeric timestamp (in milliseconds) or an ISO 8601 string. `JSON.stringify(new Date())` will return an ISO 8601 string. When you type `posts.title` as a `string`: ```typescript // instant.schema.ts const _schema = i.schema({ entities: { // ... posts: i.entity({ title: i.string(), // ... }), }, }); ``` Instant will _make sure_ that all `title` attributes are strings, and you'll get the proper TypeScript hints to boot! ### Required constraints All attributes you define are considered _required_ by default. This constraint is enforced on the backend: Instant guarantees that every entity of that type will have a value and reports errors if you attempt to add an entity without a required attribute. ```typescript const _schema = i.schema({ entities: { posts: i.entity({ title: i.string(), // <-- required published: i.date(), // <-- required }), }, }); db.transact( db.tx.posts[id()].update({ title: 'abc', // <-- no published -- will throw }), ); ``` You can mark an attribute as optional by calling `.optional()`: ```typescript const _schema = i.schema({ entities: { posts: i.entity({ title: i.string(), // <-- required published: i.date().optional(), // <-- optional }), }, }); db.transact( db.tx.posts[id()].update({ title: 'abc', // <-- no published -- still okay }), ); ``` This will also reflect in types: query results containing `posts` will show `title: string` (non-nullable) and `published: string | number | null` (nullable). You can set required on forward links, too: ```typescript postAuthor: { forward: { on: 'posts', has: 'one', label: 'author', required: true }, reverse: { on: 'profiles', has: 'many', label: 'authoredPosts' }, }, ``` Finally, for legacy attributes that are treated as required on your front-end but you are not ready to enable back-end required checks yet, you can use `.clientRequired()`. That will produce a TypeScript type without `null` but will not add a back-end required check: ```typescript const _schema = i.schema({ entities: { posts: i.entity({ title: i.string().clientRequired(), published: i.date().optional(), }), }, }); ``` ### Unique constraints Sometimes you'll want to introduce a unique constraint. For example, say we wanted to add friendly URLs to posts. We could introduce a `slug` attribute: ```typescript // instant.schema.ts const _schema = i.schema({ entities: { // ... posts: i.entity({ slug: i.string().unique(), // ... }), }, }); ``` Since we're going to use post slugs in URLs, we'll want to make sure that no two posts can have the same slug. If we mark `slug` as `unique`, _Instant will guarantee this constraint for us_. Unique attributes will also speed up queries that filter by that attribute. ```typescript const query = { posts: { $: { where: { // Since `slug` is unique, this query is 🚀 fast slug: 'completing_sicp', }, }, }, }; ``` ### Indexing attributes You can also use index attributes to speed up querying. An additional benefit is that indexed attributes can be used with comparison operators for where queries like `$gt`, `$lt`, `$gte`, and `$lte` and can be used in `order` clauses. Suppose we wanted to query for products less than $100 and order by price. First we make sure that the `price` attribute is indexed: ```typescript // instant.schema.ts const _schema = i.schema({ entities: { // ... products: i.entity({ price: i.number().indexed(), // 🔥, // ... }), }, }); ``` And now we can use `$lt` and `order` in our query: ```typescript const query = { products: { $: { where: { price: { $lt: 100 }, }, order: { price: 'desc', }, }, }, }; ``` Even if you're not using comparison operators or order clauses, indexing attributes can still speed up queries that filter by that attribute. ## 3) Links Links connect two namespaces together. When you define a link, you define it both in the 'forward', and the 'reverse' direction. For example: ```typescript postAuthor: { forward: { on: "posts", has: "one", label: "author" }, reverse: { on: "profiles", has: "many", label: "authoredPosts" }, } ``` This links `posts` and `profiles` together: - `posts.author` links to _one_ `profiles` entity - `profiles.authoredPosts` links back to _many_ `posts` entities. Since links are defined in both directions, you can query in both directions too: ```typescript // This queries all posts with their author const query1 = { posts: { author: {}, }, }; // This queries profiles, with all of their authoredPosts! const query2 = { profiles: { authoredPosts: {}, }, }; ``` Links can have one of four relationship types: `many-to-many`, `many-to-one`, `one-to-many`, and `one-to-one`. Our micro-blog example has the following relationship types: - **One-to-one** between `profiles` and `$users` - **One-to-many** between `posts` and `profiles` - **One-to-many** between `comments` and `posts` - **One-to-many** between `comments` and `profiles` - **Many-to-many** between `posts` and `tags` ### Cascade Delete Links defined with `has: "one"` can set `onDelete: "cascade"`. In this case, when the profile entity is deleted, all post entities will be deleted too: ```typescript postAuthor: { forward: { on: "posts", has: "one", label: "author", onDelete: "cascade" }, reverse: { on: "profiles", has: "many", label: "authoredPosts" }, } // this will delete profile and all linked posts db.tx.profiles[user_id].delete(); ``` Without `onDelete: "cascade"`, deleting a profile would simply delete the links but not delete the underlying posts. If you prefer to model links in the other direction, you can do it, too: ``` postAuthor: { forward: { on: "profiles", has: "many", label: "authoredPosts" }, reverse: { on: "posts", has: "one", label: "author", onDelete: "cascade" }, } ``` ## Publishing your schema Now that you have your schema, you can use the CLI to `push` it to your app: ```shell npx @interface-db/cli@latest push schema ``` The CLI will look at your app in production, show you the new columns you'd create, and run the changes for you! ``` Checking for an Instant SDK... Found [32m@interface-db/react[39m in your package.json. Found [32mNEXT_PUBLIC_INSTANT_APP_ID[39m: ***** Planning schema... The following changes will be applied to your production schema: [35mADD ENTITY[39m profiles.id [35mADD ENTITY[39m posts.id [35mADD ENTITY[39m comments.id [35mADD ENTITY[39m tags.id [32mADD ATTR[39m profiles.nickname :: unique=false, indexed=false [32mADD ATTR[39m profiles.createdAt :: unique=false, indexed=false [32mADD ATTR[39m posts.title :: unique=false, indexed=false [32mADD ATTR[39m posts.slug :: unique=true, indexed=false [32mADD ATTR[39m posts.body :: unique=false, indexed=false [32mADD ATTR[39m posts.createdAt :: unique=false, indexed=true [32mADD ATTR[39m comments.body :: unique=false, indexed=false [32mADD ATTR[39m comments.createdAt :: unique=false, indexed=false [32mADD ATTR[39m tags.title :: unique=false, indexed=false [32mADD LINK[39m posts.author <=> profiles.authoredPosts [32mADD LINK[39m comments.post <=> posts.comments [32mADD LINK[39m comments.author <=> profiles.authoredComments [32mADD LINK[39m posts.tags <=> tags.posts [32mADD LINK[39m profiles.$user <=> $users.profile [2K[34m?[39m [1mOK to proceed?[22m [36myes [32mSchema updated and [transactions](/docs/instaml) will come with typesafety out of the box. If you haven't used the CLI to push your schema yet, no problem. Any time you write `transact`, we'll automatically create missing entities for you. ## Update or Delete attributes You can always modify or delete attributes after creating them. **You can't use the CLI to do this yet, but you can use the dashboard.** Say we wanted to rename `posts.createdAt` to `posts.publishedAt`: 1. Go to your [Dashboard](https://instantdb.com/dash) 2. Click "Explorer" 3. Click "posts" 4. Click "Edit Schema" 5. Click `createdAt` You'll see a modal that you can use to rename the attribute, index it, or delete it: ## Secure your schema with permissions In the earlier sections we mentioned that new `entities` and `attributes` can be created on the fly when you call `transact`. This can be useful for development, but you may not want this in production. To prevent changes to your schema on the fly, simply add these permissions to your app. ```typescript // instant.perms.ts import type { InstantRules } from '@interface-db/react'; const rules = { attrs: { allow: { $default: 'false', }, }, } satisfies InstantRules; export default rules; ``` Once you push these permissions to production: ```bash npx @interface-db/cli@latest push perms ``` ``` Checking for an Instant SDK... Found [32m@interface-db/react[39m in your package.json. Found [32mNEXT_PUBLIC_INSTANT_APP_ID[39m: ***** Planning perms... The following changes will be applied to your perms: [31m-null[39m [32m+{[39m [32m+ attrs: {[39m [32m+ allow: {[39m [32m+ $default: "false"[39m [32m+ }[39m [32m+ }[39m [32m+}[39m [1mOK to proceed?[22m [36myes[39m[21 [32mPermissions updated![39m ``` You'll still be able to make changes in the explorer or with the CLI, but client-side transactions that try to modify your schema will fail. This means your schema is safe from unwanted changes! --- **If you've made it this far, congratulations! You should now be able to fully customize and lock down your data model. Huzzah!** # Writing data How to write data with Instant using InstaML. Instant uses a **Firebase-inspired** interface for mutations. We call our mutation language **InstaML**. ## Creating data We use the `create` action to create entities: ```typescript import { init, id } from '@interface-db/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); // transact! 🔥 db.transact(db.tx.goals[id()].create({ title: 'eat' })); ``` This creates a new `goal` with the following properties: - It's identified by a randomly generated id via the `id()` function. - It has an attribute `title` with value `eat`. You can store `strings`, `numbers`, `booleans`, `arrays`, and `objects` as values. You can also generate values via functions. Below is an example for picking a random goal title. ```javascript db.transact( db.tx.goals[id()].create({ title: ['eat', 'sleep', 'hack', 'repeat'][Math.floor(Math.random() * 4)], }), ); ``` ## Update data The `update` action is used for updating entities. Suppose we had created the following goal: ```javascript const eatId = id(); db.transact( db.tx.goals[eatId].update({ priority: 'top', lastTimeEaten: 'Yesterday' }), ); ``` We eat some food and decide to update the goal. We can do that like so: ```javascript db.transact(db.tx.goals[eatId].update({ lastTimeEaten: 'Today' })); ``` This will only update the value of the `lastTimeEaten` attribute for entity `eat`. As with NoSQL, you don't need to use the same schema for each entity in a namespace. After creating the previous goal, you can run the following: ```javascript db.transact( db.tx.goals[id()].update({ priority: 'none', isSecret: true, value: 10, aList: [1, 2, 3], anObject: { foo: 'bar' }, }), ); ``` The `update` function creates or updates an entity depending on whether it already exists (so-called "upsert" mode). If the entity doesn’t exist yet, calling `update` will create it; otherwise, it will update it. To force “strict update” mode, pass the `{ upsert: false }` option: ```javascript db.transact( db.tx.goals[eatId].update({ lastTimeEaten: 'Today' }, { upsert: false }), ); ``` ## Merge data When you `update` an attribute, you overwrite it. This is fine for updating values of strings, numbers, and booleans. But if you use `update` to overwrite JSON objects, you may encounter two problems: 1. You lose any data you didn't specify. 2. You risk clobbering changes made by other clients. For example, imagine we had a `game` entity that stored a `state` of favorite colors: ```javascript // User 1 saves {'0-0': 'red'} db.transact(db.tx.games[gameId].update({ state: { '0-0': 'red' } })); // User 2 saves {'0-1': 'blue'} db.transact(db.tx.games[gameId].update({ state: { '0-1': 'blue' } })); // 🤔 Uh oh! User 2 overwrites User 1's changes: // Final State: {'0-1': 'blue' } ``` To make working with deeply nested, document-style JSON values a breeze, we created `merge`. Similar to [lodash's `merge` function](https://lodash.com/docs/4.17.15#merge), `merge` allows you to specify the slice of data you want to update: ```javascript // User 1 saves {'0-0': 'red'} db.transact(db.tx.games[gameId].merge({ state: { '0-0': 'red' } })); // User 2 saves {'0-1': 'blue'} db.transact(db.tx.games[gameId].merge({ state: { '0-1': 'blue' } })); // ✅ Wohoo! Both states are merged! // Final State: {'0-0': 'red', '0-1': 'blue' } ``` `merge` only merges objects. Calling `merge` on **arrays, numbers, or booleans** will overwrite the values. Sometimes you may want to remove keys from a nested object. You can do so by calling `merge` with a key set to `null`. This will remove the corresponding property from the object. Setting a key to `undefined` will have no effect. Set the key to `null` to remove the property. ```javascript // State: {'0-0': 'red', '0-1': 'blue' } db.transact(db.tx.games[gameId].merge({ state: { '0-1': null } })); // New State! {'0-0': 'red' } ``` ## Delete data The `delete` action is used for deleting entities. ```javascript db.transact(db.tx.goals[eatId].delete()); ``` You can generate an array of `delete` txs to delete all entities in a namespace: ```javascript const { isLoading, error, data } = db.useQuery({ goals: {} }); const { goals } = data; // ... db.transact(goals.map((g) => db.tx.goals[g.id].delete())); ``` Calling `delete` on an entity also deletes its associations. So no need to worry about cleaning up previously created links. ## Link data `link` is used to create associations. Suppose we create a `goal` and a `todo`. ```javascript db.transact([ db.tx.todos[workoutId].update({ title: 'Go on a run' }), db.tx.goals[healthId].update({ title: 'Get fit!' }), ]); ``` We can associate `healthId` with `workoutId` like so: ```javascript db.transact(db.tx.goals[healthId].link({ todos: workoutId })); ``` We could have done all this in one `transact` too via chaining transaction chunks. ```javascript db.transact([ db.tx.todos[workoutId].update({ title: 'Go on a run' }), db.tx.goals[healthId] .update({ title: 'Get fit!' }) .link({ todos: workoutId }), ]); ``` You can specify multiple ids in one `link` as well: ```javascript db.transact([ db.tx.todos[workoutId].update({ title: 'Go on a run' }), db.tx.todos[proteinId].update({ title: 'Drink protein' }), db.tx.todos[sleepId].update({ title: 'Go to bed early' }), db.tx.goals[healthId] .update({ title: 'Get fit!' }) .link({ todos: [workoutId, proteinId, sleepId] }), ]); ``` Links are bi-directional. Say we link `healthId` to `workoutId`: ```javascript db.transact(db.tx.goals[healthId].link({ todos: workoutId })); ``` We can query associations in both directions: ```javascript const { isLoading, error, data } = db.useQuery({ goals: { todos: {} }, todos: { goals: {} }, }); const { goals, todos } = data; console.log('goals with nested todos', goals); console.log('todos with nested goals', todos); ``` ## Unlink data Links can be removed via `unlink`. ```javascript db.transact(db.tx.goals[healthId].unlink({ todos: workoutId })); ``` This removes links in both directions. Unlinking can be done in either direction so unlinking `workoutId` from `healthId` would have the same effect. ```javascript db.transact([db.tx.todos[workoutId].unlink({ goals: healthId })]); ``` We can `unlink` multiple ids too: ```javascript db.transact([ db.tx.goals[healthId].unlink({ todos: [workoutId, proteinId, sleepId] }), db.tx.goals[workId].unlink({ todos: [standupId, reviewPRsId, focusId] }), ]); ``` ## Lookup by unique attribute If your entity has a unique attribute, you can use `lookup` in place of the id to perform updates. ```javascript db.transact( db.tx.profiles.lookup('email', 'eva_lu_ator@instantdb.com').update({ name: 'Eva Lu Ator', }), ); ``` The `lookup` function takes the attribute as its first argument and the unique attribute value as its second argument. When it is used in a transaction, the updates will be applied to the entity that has the unique value. If no entity has the value, then a new entity with a random id will be created with the value. It can be used with `update`, `delete`, `merge`, `link`, and `unlink`. ## Lookups in links When used with links, it can also be used in place of the linked entity's id. ```javascript db.transact( db.tx.users.lookup('email', 'eva_lu_ator@instantdb.com').link({ posts: lookup('number', 15), // using a lookup in place of the id }), ); ``` ## Transacts are atomic When you call `db.transact`, all the transactions are committed atomically. If any of the transactions fail, none of them will be committed. ## Typesafety By default, `db.transact` is permissive. When you save data, we'll create missing attributes for you: ```typescript db.tx.todos[workoutId].update({ // Instant will automatically create this attribute dueDate: Date.now() + 60 * 1000, }); ``` As your app grows, you may want to start enforcing types. When you're ready, you can start using a [schema](/docs/modeling-data). If your schema includes a `todos.dueDate` for example: ```typescript // instant.schema.ts const _schema = i.schema({ entities: { todos: i.entity({ // ... dueDate: i.date(), }), }, // ... }); // ... ``` Instant will enforce that `todos.dueDate` values are actually dates, and you'll get some nice IntelliSense to boot: Instant also comes with a few utility types, which can help you write abstractions over `transact`. For example, say you wanted to write a custom `update` function: ```typescript // Goal myCustomUpdate('todos', { dueDate: Date.now() }); ``` You can use the `UpdateParams` utility to make sure arguments follow the schema: ```typescript import { UpdateParams } from '@interface-db/react'; import { AppSchema } from '../instant.schema.ts'; type EntityTypes = keyof AppSchema['entities']; function myCustomUpdate ( etype: EType, args: UpdateParams , ) { // .. } ``` And the `LinkParams` utility can do the same for links: ```typescript import { LinkParams } from '@interface-db/react'; import { AppSchema } from '../instant.schema.ts'; type EntityTypes = keyof AppSchema['entities']; function myCustomLink ( etype: EType, args: LinkParams , ) { // .. } ``` To learn more about writing schemas, check out the [Modeling Data](/docs/modeling-data) section. ## Batching transactions If you have a large number of transactions to commit, you'll want to batch them to avoid hitting transaction limits and timeouts. Suppose we want to create 3000 goals. Here's how we can batch them into 30 transactions of 100 goals each. ```javascript const batchSize = 100; // doing 100 txs should be pretty safe const createGoals = async (total) => { let goals = []; const batches = []; // iterate through all your goals and create batches for (let i = 0; i < total; i++) { const goalNumber = i + 1; goals.push( db.tx.goals[id()].update({ goalNumber, title: `Goal ${goalNumber}` }), ); // We have enough goals to create a batch if (goals.length >= batchSize) { batches.push(goals); goals = []; // reset goals for the next batch } } // Add any remaining goals to the last batch if (goals.length) { batches.push(goals); } // Now that you have your batches, transact them for (const batch of batches) { await db.transact(batch); } }; ``` ## Using the tx proxy object `db.tx` is a [proxy object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) which creates transaction chunks to be committed via `db.transact`. It follows the format: ``` db.tx.NAMESPACE_LABEL[ENTITY_IDENTIFIER].ACTION(ACTION_SPECIFIC_DATA) ``` - `NAMESPACE_LABEL` refers to the namespace to commit (e.g. `goals`, `todos`) - `ENTITY_IDENTIFIER` is the id to look up in the namespace. This id must be a uuid and unique to the namespace. You can use the `id()` function to generate a uuid for convenience. - `ACTION` is one of `create`, `update`, `merge`, `delete`, `link`, `unlink` - `ACTION_SPECIFIC_DATA` depends on the action - `create` and `update` take in an object of information to commit - `merge` takes in an object to deep merge with the existing data - `delete` is the only action that doesn't take in any data - `link` and `unlink` take an object of label-entity pairs to create/delete associations # Reading data How to read data with Instant using InstaQL. Instant uses a declarative syntax for querying. It's like GraphQL without the configuration. Here's how you can query data with **InstaQL.** ## Fetch namespace One of the simplest queries you can write is to simply get all entities of a namespace. ```javascript import { init } from '@interface-db/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { // Queries! 🚀 const query = { goals: {} }; const { isLoading, error, data } = db.useQuery(query); // ... } ``` Inspecting `data`, we'll see: ```javascript console.log(data) { "goals": [ { "id": healthId, "title": "Get fit!" }, { "id": workId, "title": "Get promoted!" } ] } ``` For comparison, the SQL equivalent of this would be something like: ```javascript const data = { goals: doSQL('SELECT * FROM goals') }; ``` ## Fetch multiple namespaces You can fetch multiple namespaces at once: ```javascript const query = { goals: {}, todos: {} }; const { isLoading, error, data } = db.useQuery(query); ``` We will now see data for both namespaces. ```javascript console.log(data) { "goals": [...], "todos": [ { "id": focusId, "title": "Code a bunch" }, { "id": proteinId, "title": "Drink protein" }, ... ] } ``` The equivalent of this in SQL would be to write two separate queries. ```javascript const data = { goals: doSQL('SELECT * from goals'), todos: doSQL('SELECT * from todos'), }; ``` ## Fetch a specific entity If you want to filter entities, you can use the `where` keyword. Here we fetch a specific goal. ```javascript const query = { goals: { $: { where: { id: healthId, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "goals": [ { "id": healthId, "title": "Get fit!" } ] } ``` The SQL equivalent would be: ```javascript const data = { goals: doSQL("SELECT * FROM goals WHERE id = 'healthId'") }; ``` ## Fetch associations We can fetch goals and their related todos. ```javascript const query = { goals: { todos: {}, }, }; const { isLoading, error, data } = db.useQuery(query); ``` `goals` would now include nested `todos` ```javascript console.log(data) { "goals": [ { "id": healthId, "title": "Get fit!", "todos": [...], }, { "id": workId, "title": "Get promoted!", "todos": [...], } ] } ``` ### Comparing with SQL The SQL equivalent for this would be something along the lines of: ```javascript const query = ` SELECT g.*, gt.todos FROM goals g JOIN ( SELECT g.id, json_agg(t.*) as todos FROM goals g LEFT JOIN todos t on g.id = t.goal_id GROUP BY 1 ) gt on g.id = gt.id `; const data = { goals: doSQL(query) }; ``` Notice the complexity of this SQL query. Although fetching associations in SQL is straightforward via `JOIN`, marshalling the results in a nested structure via SQL is tricky. An alternative approach would be to write two straight-forward queries and then marshall the data on the client. ```javascript const _goals = doSQL("SELECT * from goals") const _todos = doSQL("SELECT * from todos") const data = {goals: _goals.map(g => ( return {...g, todos: _todos.filter(t => t.goal_id === g.id)} )) ``` Now compare these two approaches with `InstaQL`: ```javascript const query = { goals: { todos: {}, }, }; const { isLoading, error, data } = db.useQuery(query); ``` Modern applications often need to render nested relations. `InstaQL` really starts to shine for these use cases. ## Fetch specific associations ### A) Fetch associations for filtered namespace We can fetch a specific entity in a namespace as well as its related associations. ```javascript const query = { goals: { $: { where: { id: healthId, }, }, todos: {}, }, }; const { isLoading, error, data } = db.useQuery(query); ``` This returns: ```javascript console.log(data) { "goals": [ { "id": healthId, "title": "Get fit!", "todos": [ { "id": proteinId, "title": "Drink protein" }, { "id": sleepId, "title": "Go to bed early" }, { "id": workoutId, "title": "Go on a run" } ] } ] } ``` ### B) Filter namespace by associated values We can filter namespaces **by their associations**: ```javascript const query = { goals: { $: { where: { 'todos.title': 'Code a bunch', }, }, todos: {}, }, }; const { isLoading, error, data } = db.useQuery(query); ``` This returns: ```javascript console.log(data) { "goals": [ { "id": workId, "title": "Get promoted!", "todos": [ { "id": focusId, "title": "Code a bunch" }, { "id": reviewPRsId, "title": "Review PRs" }, { "id": standupId, "title": "Do standup" } ] } ] } ``` ### C) Filter associations We can also filter associated data. ```javascript const query = { goals: { todos: { $: { where: { title: 'Go on a run', }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` This will return goals and filtered todos: ```javascript console.log(data) { "goals": [ { "id": healthId, "title": "Get fit!", "todos": [ { "id": workoutId, "title": "Go on a run" } ] }, { "id": workId, "title": "Get promoted!", "todos": [] } ] } ``` --- Notice the difference between these three cases. - A) Fetched all todos for goal with id `health` - B) Filtered goals with at least one todo titled `Code a bunch` - C) Fetched all goals and filtered associated todos by title `Go on a run` --- ## Inverse Associations Associations are also available in the reverse order. ```javascript const query = { todos: { goals: {}, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "todos": [ { "id": focusId, "title": "Code a bunch", "goals": [ { "id": workId, "title": "Get promoted!" } ] }, ..., ] } ``` ## Defer queries You can also defer queries until a condition is met. This is useful when you need to wait for some data to be available before you can run your query. Here's an example of deferring a fetch for todos until a user is logged in. ```javascript const { isLoading, user, error } = db.useAuth(); const { isLoading: isLoadingTodos, error, data, } = db.useQuery( user ? { // The query will run once user is populated todos: { $: { where: { 'owner.id': user.id, }, }, }, } : // Otherwise skip the query, which sets `isLoading` to true null, ); ``` **NOTE:** Passing `null` to `db.useQuery` will result in `isLoading` being true. In the example above, this means that `isLoadingTodos` will _always be true_ if the user is not logged in. ## Pagination You can limit the number of items from a top-level namespace by adding a `limit` to the option map: ```javascript const query = { todos: { // limit is only supported for top-level namespaces right now // and not for nested namespaces. $: { limit: 10 }, }, }; const { isLoading, error, data, pageInfo } = db.useQuery(query); ``` Instant supports both offset-based and cursor-based pagination for top-level namespaces. ### Offset To get the next page, you can use an offset: ```javascript const query = { todos: { $: { limit: 10, // similar to `limit`, `offset` is only supported for top-level namespaces offset: 10, }, }, }; const { isLoading, error, data, pageInfo } = db.useQuery(query); ``` In a React application, your offset-based pagination code might look something like this: ```jsx const [pageNumber, setPageNumber] = React.useState(1); const pageSize = 10; const query = { todos: { $: { limit: pageSize, offset: pageSize * (pageNumber - 1), }, }, }; const { isLoading, error, data } = db.useQuery(query); // Load the next page by increasing the page number, which will // increase the offset by the page size. const loadNextPage = () => { setPageNumber(pageNumber + 1); }; // Load the previous page by decreasing the page number, which will // decrease the offset by the page size. const loadPreviousPage = () => { setPageNumber(pageNumber - 1); }; ``` ### Cursors You can also get the next page with the `endCursor` returned in the `pageInfo` map from the previous result: ```javascript const query = { todos: { $: { // These also are only supported for top-level namespaces first: 10, after: pageInfo?.todos?.endCursor, }, }, }; ``` To get the previous page, use the `startCursor` in the `before` field of the option map and ask for the `last` items: ```javascript const query = { todos: { $: { last: 10, before: pageInfo?.todos?.startCursor, }, }, }; ``` In a React application, your cursor-based pagination code might look something like this: ```jsx const pageSize = 10; const [cursors, setCursors] = React.useState({ first: pageSize }); const query = { todos: { $: { ...cursors, }, }, }; const { isLoading, error, data, pageInfo } = db.useQuery(query); const loadNextPage = () => { const endCursor = pageInfo?.todos?.endCursor; if (endCursor) { setCursors({ after: endCursor, first: pageSize }); } }; const loadPreviousPage = () => { const startCursor = pageInfo?.todos?.startCursor; if (startCursor) { setCursors({ before: startCursor, // Ask for the `last` 10 items so that we get the items just // before our startCursor last: pageSize, }); } }; ``` ### Ordering The default ordering is by the time the objects were created, in ascending order. You can change the order with the `order` key in the option map for namespaces: ```javascript const query = { todos: { $: { limit: 10, // Similar to limit, order is limited to top-level namespaces right now order: { serverCreatedAt: 'desc', }, }, }, }; ``` The `serverCreatedAt` field is a reserved key that orders by the time that the object was first persisted on the Instant backend. It can take the value 'asc' (the default) or 'desc'. You can also order by any attribute that is indexed and has a checked type. Add indexes and checked types to your attributes from the [Explorer on the Instant dashboard](/dash?t=explorer) or from the [cli](/docs/cli). ```typescript // Get the todos that are due next const query = { todos: { $: { limit: 10, where: { dueDate: { $gt: Date.now() }, }, order: { dueDate: 'asc', }, }, }, }; ``` You can use order in nested namespaces as well: ```typescript // Get goals with their associated todos ordered by due date const query = { goals: { todos: { $: { order: { dueDate: 'asc', }, }, }, }, }; ``` Order is not supported on nested attributes. So if todos had an owner you could not order todos by "owner.name". This behavior is different from `where`, which supports filtering on nested attributes. ```typescript // ❌ Order does not support nested attributes const query = { todos: { $: { order: { 'owner.name': 'asc', // Cannot order by nested attributes }, }, }, }; // ✅ Where does support filtering on nested attributes const query = { todos: { $: { where: { 'owner.name': 'alyssa.p.hacker@instantdb.com', }, }, }, }; ``` ## Advanced filtering ### Multiple `where` conditions The `where` clause supports multiple keys which will filter entities that match all of the conditions. ```javascript const query = { todos: { $: { where: { completed: true, 'goals.title': 'Get promoted!', }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "todos": [ { "id": focusId, "title": "Code a bunch", "completed": true } ] } ``` ### And The `where` clause supports `and` queries which are useful when you want to filter entities that match multiple associated values. In this example, we want to find goals that have todos with the titles `Drink protein` and `Go on a run`: ```javascript const query = { goals: { $: { where: { and: [ { 'todos.title': 'Drink protein' }, { 'todos.title': 'Go on a run' }, ], }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "goals": [ { "id": healthId, "title": "Get fit!" } ] } ``` ### OR The `where` clause supports `or` queries that will filter entities that match any of the clauses in the provided list: ```javascript const query = { todos: { $: { where: { or: [{ title: 'Code a bunch' }, { title: 'Review PRs' }], }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data); { "todos": [ { "id": focusId, "title": "Code a bunch" }, { "id": reviewPRsId, "title": "Review PRs" }, ] } ``` ### $in The `where` clause supports `$in` queries that will filter entities that match any of the items in the provided list. You can think of this as a shorthand for `or` on a single key. ```javascript const query = { todos: { $: { where: { title: { $in: ['Code a bunch', 'Review PRs'] }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "todos": [ { "id": focusId, "title": "Code a bunch" }, { "id": reviewPRsId, "title": "Review PRs" } ] } ``` ### Comparison operators The `where` clause supports comparison operators on fields that are indexed and have checked types. Add indexes and checked types to your attributes from the [Explorer on the Instant dashboard](/dash?t=explorer) or from the [cli with Schema-as-code](/docs/modeling-data). | Operator | Description | JS equivalent | | :------: | :----------------------: | :-----------: | | `$gt` | greater than | `>` | | `$lt` | less than | `<` | | `$gte` | greater than or equal to | `>=` | | `$lte` | less than or equal to | `<=` | ```javascript const query = { todos: { $: { where: { timeEstimateHours: { $gt: 24 }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data); { "todos": [ { "id": buildShipId, "title": "Build a starship prototype", "timeEstimateHours": 5000 } ] } ``` Dates can be stored as timestamps (milliseconds since the epoch, e.g. `Date.now()`) or as ISO 8601 strings (e.g. `JSON.stringify(new Date())`) and can be queried in the same formats: ```javascript const now = '2024-11-26T15:25:00.054Z'; const query = { todos: { $: { where: { dueDate: { $lte: now } } }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data); { "todos": [ { "id": slsFlightId, "title": "Space Launch System maiden flight", "dueDate": "2017-01-01T00:00:00Z" } ] } ``` If you try to use comparison operators on data that isn't indexed and type-checked, you'll get an error: ```javascript const query = { todos: { $: { where: { priority: { $gt: 2 } } }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(error); { "message": "Validation failed for query", "hint": { "data-type": "query", "errors": [ { "expected?": "indexed?", "in": ["priority", "$", "where", "priority"], "message": "The `todos.priority` attribute must be indexed to use comparison operators." } ], "input": { "todos": { "$": { "where": { "priority": { "$gt": 2 } } } } } } } ``` ### $ne The `where` clause supports `$ne` queries that will return entities that don't match the provided value for the field, including entities where the field is null or undefined. ```javascript const query = { todos: { $: { where: { location: { $ne: 'work' }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "todos": [ { "id": cookId, "title": "Cook dinner", "location": "home" }, { "id": readId, "title": "Read", "location": null }, { "id": napId, "title": "Take a nap" } ] } ``` ### $isNull The `where` clause supports `$isNull` queries that will filter entities by whether the field value is either null or undefined. Set `$isNull` to `true` to return entities where the field is null or undefined. Set `$isNull` to `false` to return entities where the field is not null and not undefined. ```javascript const query = { todos: { $: { where: { location: { $isNull: false }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "todos": [ { "id": cookId, "title": "Cook dinner", "location": "home" } ] } ``` ```javascript const query = { todos: { $: { where: { location: { $isNull: true }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "todos": [ { "id": readId, "title": "Read", "location": null }, { "id": napId, "title": "Take a nap" } ] } ``` ### $like The `where` clause supports `$like` on fields that are indexed with a checked `string` type. `$like` queries will return entities that match a **case-sensitive** substring of the provided value for the field. For **case-insensitive** matching, use `$ilike` in place of `$like`. Here's how you can do queries like `startsWith`, `endsWith` and `includes`. | Example | Description | JS equivalent | | :-----------------------: | :-------------------: | :-----------: | | `{ $like: "Get%" }` | Starts with 'Get' | `startsWith` | | `{ $like: "%promoted!" }` | Ends with 'promoted!' | `endsWith` | | `{ $like: "%fit%" }` | Contains 'fit' | `includes` | Here's how you can use `$like` to find all goals that end with the word "promoted!" ```javascript // Find all goals that end with the word "promoted!" const query = { goals: { $: { where: { title: { $like: '%promoted!' }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "goals": [ { "id": workId, "title": "Get promoted!", } ] } ``` You can use `$like` in nested queries as well: ```javascript // Find goals that have todos with the word "standup" in their title const query = { goals: { $: { where: { 'todos.title': { $like: '%standup%' }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` This returns: ```javascript console.log(data) { "goals": [ { "id": standupId, "title": "Perform standup!", } ] } ``` Case-insensitive matching with `$ilike`: ```javascript const query = { goals: { $: { where: { 'todos.title': { $ilike: '%stand%' }, }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "goals": [ { "id": standupId, "title": "Perform standup!", }, { "id": standId, "title": "Stand up a food truck.", } ] } ``` ## Select fields An InstaQL query will fetch all fields for each object. If you prefer to select the specific fields that you want your query to return, use the `fields` param: ```javascript const query = { goals: { $: { fields: ['status'], }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "goals": [ { "id": standupId, // id will always be returned even if not specified "status": "in-progress" }, { "id": standId, "status": "completed" } ] } ``` `fields` also works with nested relations: ```javascript const query = { goals: { $: { fields: ['title'], }, todos: { $: { fields: ['id'], }, }, }, }; const { isLoading, error, data } = db.useQuery(query); ``` ```javascript console.log(data) { "goals": [ { "id": standupId, "title": "Perform standup!", "todos": [{"id": writeJokesId}, {"id": goToOpenMicId}] }, { "id": standId, "title": "Stand up a food truck.", "todos": [{"id": learnToCookId}, {"id": buyATruckId}] } ] } ``` Using `fields` can be useful for performance optimization. It reduces the amount of data that needs to be transferred from the server and minimizes the number of re-renders in your React application if there are no changes to your selected fields. Using `fields` doesn't restrict a client from doing a full query. If you have sensitive data on your entities that you don't want to expose, you'll want to use [permissions](/docs/permissions#fields) to restrict access. ## Typesafety By default, `db.useQuery` is permissive. You don't have to tell us your schema upfront, and you can write any kind of query: ```typescript const query = { goals: { todos: {}, }, }; const { isLoading, error, data } = db.useQuery(query); ``` As your app grows, you may want to start enforcing types. When you're ready you can write a [schema](/docs/modeling-data). If your schema includes `goals` and `todos` for example: ```typescript // instant.schema.ts import { i } from '@interface-db/react'; const _schema = i.schema({ entities: { goals: i.entity({ title: i.string(), }), todos: i.entity({ title: i.string(), text: i.string(), done: i.boolean(), createdAt: i.date(), dueDate: i.date(), }), }, links: { goalsTodos: { forward: { on: 'goals', has: 'many', label: 'todos' }, reverse: { on: 'todos', has: 'many', label: 'goals' }, }, }, }); // This helps TypeScript display better IntelliSense type _AppSchema = typeof _schema; interface AppSchema extends _AppSchema {} const schema: AppSchema = _schema; export type { AppSchema }; export default schema; ``` Instant will start giving you IntelliSense for your queries. For example, if you're querying for goals, you'll see that only `todos` can be associated: And if you hover over `data`, you'll see the actual typed output of your query: ### Utility Types Instant also comes with some utility types to help you use your schema in TypeScript. For example, you could define your `query` upfront: ```typescript import { InstaQLParams } from '@interface-db/react'; import { AppSchema } from '../instant.schema.ts'; // `query` typechecks against our schema! const query = { goals: { todos: {} }, } satisfies InstaQLParams ; ``` Or you can define your result type: ```typescript import { InstaQLResult } from '@interface-db/react'; import { AppSchema } from '../instant.schema.ts'; type GoalsTodosResult = InstaQLResult ; ``` Or you can extract a particular entity: ```typescript import { InstaQLEntity } from '@interface-db/react'; import { AppSchema } from '../instant.schema.ts'; type Todo = InstaQLEntity ; ``` You can specify links relative to your entity: ```typescript type TodoWithGoals = InstaQLEntity ; ``` To learn more about writing schemas, check out the [Modeling Data](/docs/modeling-data) section. ## Query once Sometimes, you don't want a subscription and just want to fetch data once. For example, you might want to fetch data before rendering a page or check whether a username is available. In these cases, you can use `queryOnce` instead of `useQuery`. `queryOnce` returns a promise that resolves with the data once the query is complete. Unlike `useQuery`, `queryOnce` will throw an error if the user is offline. This is because `queryOnce` is intended for use cases where you need the most up-to-date data. ```javascript const query = { todos: {} }; const { data } = await db.queryOnce(query); // returns the same data as useQuery, but without the isLoading and error fields ``` You can also do pagination with `queryOnce`: ```javascript const query = { todos: { $: { limit: 10, offset: 10, }, }, }; const { data, pageInfo } = await db.queryOnce(query); // pageInfo behaves the same as with useQuery ``` # Infinite Queries How to subscribe to large amounts of data incrementally It is a common pattern to show users a long list of items that gets larger as they interact. Instant supports this pattern with the `useInfiniteQuery` hook. The useInfiniteQuery hook is designed to work just like the useQuery hook. You make the same kind of queries, and it returns the same kind of data. But now you get a canLoadNextPage variable and a loadNextPage function to load more items. ## Example This example demonstrates a typical social media feed where the newest posts are at the top. ```ts import { init } from '@interface-db/react'; import { db } from "../db" import { Post } from "../components/Post" function HomePage() { const { data, canLoadNextPage, loadNextPage, isLoading, error } = db.useInfiniteQuery({ posts: { $: { limit: 50, // Load 50 posts at a time order: { createdAt: 'desc', }, }, }, }); if (isLoading) { return Loading...; } if (error) { returnError: {error.message}; } return ({data.posts.map((post) => (); } ``` Any new posts that get created will automatically appear at the top, and as the user scrolls, the `loadNextPage` function can load older posts as needed. ## Reactivity Just like `useQuery`, all data returned is fully reactive. Updates and deletions will appear immediately. New items added to the "start" of the query will show up automatically, as will items added anywhere in the middle. Items that _would_ be ordered at the very end of the results will show up if the limit for that page has not yet been reached. For example, with `{todos: {$: {limit: 20, order: createdAt: "asc"}}}` (showing oldest todos first) If there are already 19 todos in the database, an additional todo getting created will show up in the result automatically, and the next todo after that will set "canLoadNextPage" to true, requiring the `loadNextPage` function to be called. When the new page is added, any newly created todos will show up at the end automatically until 40 total todos are shown. Changing any part of the query will result in a full reset of all data, returning back to a state with only one page loaded. ## Vanilla JS The `@interface-db/core` library also supports making infinite queries with the same syntax as `db.subscribeQuery()`. Example: ```ts const { unsubscribe, loadNextPage } = db.subscribeInfiniteQuery( { posts: { $: { limit: 20, // Load 20 posts at a time order: { createdAt: 'desc', }, }, }, }, (resp) => { console.log('Posts: ', resp.data.posts); console.log('Can Load More ?', resp.canLoadNextPage); }, ); ``` # Instant on the Backend How to use Instant on the server with the Admin SDK. You can use Instant on the server as well! This can be especially useful for running scripts, custom auth flows, or sensitive application logic. We currently offer two server-side SDKs: - **JavaScript:** `@interface-db/admin` — the focus of this page. - **Python:** `instantdb` — see [Getting started with Python](/docs/start-python) for the equivalent surface in Python. ## Admin SDK `@interface-db/admin` is the JavaScript Admin SDK for using Instant in a non-browser context. This library is similar to our client SDK with a few tweaks. ### init ```javascript import { init, id } from '@interface-db/admin'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID, adminToken: process.env.INSTANT_APP_ADMIN_TOKEN, }); ``` As with `@interface-db/react`, you must `init` before doing any queries or writes. Running `init` authenticates you against our admin API. In addition to providing your `appId`, you can provide your `adminToken`. Whereas exposing your `appId` in source control is fine, it's not safe to expose your `adminToken`. Permission checks will not run for queries and writes from our admin API. Be sure to regenerate your token from your dashboard if it accidentally leaks. ## Reading and Writing Data `query` and `transact` let you read and write data as an admin. ### query ```javascript const data = await db.query({ goals: {}, todos: {} }); const { goals, todos } = data; ``` In React we use `db.useQuery` to enable "live queries", queries that will automatically update when data changes. In the admin SDK we instead use an async `db.query` function that simply fires a query once and returns a result. ### transact ```typescript const res = await db.transact([db.tx.todos[id()].update({ title: 'Get fit' })]); console.log('New todo entry made for with tx-id', res['tx-id']); ``` `db.transact` is an async function that behaves nearly identically to `db.transact` from `@interface-db/react`. It returns a `tx-id` on success. ## Subscriptions on the backend You can use `db.subscribeQuery` to subscribe to queries on the backend. This can be useful if you have backend processes that react to database changes. For example, let's say we wanted to subscribe to a `tasks` table. ### With callbacks You could pass in a callback to `db.subscribeQuery` that gets called with newly updated query results: ```typescript const sub = db.subscribeQuery({ tasks: { $: { limit: 10 } } }, (payload) => { if (payload.type === 'error') { console.log('error', error); sub.close(); } else { console.log('got data!', payload.data); } }); // When you want to close the subscription: sub.close(); ``` ### With async iterator Or if you prefer, you can skip providing a callback and use async iterators: ```typescript const sub = db.subscribeQuery({ tasks: { $: { limit: 10 } } }); for await (const payload of sub) { if (payload.type === 'error') { console.log('error', error); sub.close(); } else { console.log('data', payload.data); } } // When you want to close the subscription: sub.close(); ``` Subscriptions keep a live connection open on your backend. Be sure to close them when they’re no longer needed to avoid tying up resources unnecessarily. ## Schema `init` also accepts a schema argument: ```typescript import { init, id } from '@interface-db/admin'; import schema from '../instant.schema.ts'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID, adminToken: process.env.INSTANT_APP_ADMIN_TOKEN, schema, }); ``` If you add a schema, `db.query` and `db.transact` will come with autocompletion and typesafety out of the box. The backend will also use your schema to generate missing attributes. To learn more about writing schemas, head on over to the [Modeling your data](/docs/modeling-data) section. ## Impersonating users When you use the admin SDK, you can make _any_ query or transaction. As an admin, you bypass permissions. But sometimes you want to make queries on behalf of your users and would like to respect permissions. You can do this with the `db.asUser` function. ```typescript // Scope by their email const scopedDb = db.asUser({ email: 'alyssa_p_hacker@instantdb.com' }); // Or with their auth token const token = db.auth.createToken({ email: 'alyssa_p_hacker@instantdb.com' }); const scopedDb = db.asUser({ token }); // Or use the db as a guest! const scopedDb = db.asUser({ guest: true }); // Queries and transactions will run with those permissions await scopedDb.query({ logs: {} }); ``` ### Running the admin SDK in client environments Impersonation can also let you run the Admin SDK _without_ exposing an admin token. ```javascript import { init } from '@interface-db/admin'; // If you only impersonate with a user token or as a guest, // you can _skip_ admin credentials const db = init({ appId: process.env.INSTANT_APP_ID! }); // Pass in a user token to run as a particular user const userDB = db.asUser({ token: "...", }); // Or run as a guest const guestDB = db.asUser({ guest: true, }); // Queries and transactions will work with the respective permissions await userDB.query({ todos: {} }); await guestDB.query({ publicData: {} }); ``` This approach is perfect for places where you need something like the Admin SDK, but don't want to expose admin credentials. For example, what if you want to run a background daemon on a user's machine? You can't use the client SDK, because you don't need optimistic updates. You wouldn't want to provide the admin token, because this code runs on a user's machine. That's when impersonation really comes in handy. Without an `adminToken`, you must use `.asUser({ token })` or `asUser({ guest: true })` for all operations. Direct queries and transactions on the base `db` instance will fail, and you won't be able to impersonate with `asUser({ email })`. In protected environments we definitely recommend including the `adminToken`. ## Retrieve a user As an admin, you can retrieve an app user record by `email`, `id`, or `refresh_token`. You can do this with the `db.auth.getUser` function. ```typescript const user = await db.auth.getUser({ email: 'alyssa_p_hacker@instantdb.com' }); const user = await db.auth.getUser({ id: userId, }); const user = await db.auth.getUser({ refresh_token: userRefreshToken, }); ``` ## Delete a user You can also delete an app user record by `email`, `id`, or `refresh_token`. You can do this with the `db.auth.deleteUser` function. ```typescript const deletedUser = await db.auth.deleteUser({ email: 'alyssa_p_hacker@instantdb.com', }); const deletedUser = await db.auth.deleteUser({ id: userId, }); const deletedUser = await db.auth.deleteUser({ refresh_token: userRefreshToken, }); ``` Note that this _only_ deletes the user record and any associated data with cascade on delete. If there's additional data you need to clean up, you'll need to do it manually: ```typescript const { goals, todos } = await db.query({ goals: { $: { where: { creator: userId } } }, todos: { $: { where: { creator: userId } } }, }); await db.transact([ ...goals.map((item) => db.tx.goals[item.id].delete()), ...todos.map((item) => tx.todos[item.id].delete()), ]); // Now we can delete the user await db.auth.deleteUser({ id: userId }); ``` ## Presence in the Backend If you use [rooms & presence](/docs/presence-and-topics), you may want to query for the data currently in a room with the admin API. This can be especially useful if you are sending a notification, for example, and want to skip it if the user is already online. To get room data from the admin API, use `db.rooms.getPresence`: ```typescript const data = await db.rooms.getPresence('chat', 'room-123'); console.log(Object.values(data)); // [{ // 'peer-id': '...', // user: { id: '...', email: 'foo@bar.com', ... }, // data: { typing: true, ... }, // }, // }]; ``` ## Sign Out The `db.auth.signOut` method allows you to log out users. You can log a user out from every session by passing in their `email`, or `id`. Or you can log a user out from a particular session by passing in a `refresh_token`: ```typescript // All sessions for this email sign out await db.auth.signOut({ email: 'alyssa_p_hacker@instantdb.com' }); // All sessions for this user id sign out const user = await db.auth.signOut({ id: userId, }); // Just sign out the session for this refresh token await db.auth.signOut({ refresh_token: userRefreshToken, }); ``` ## Custom Auth You can use the Admin SDK to create your own authentication flows. To implement custom auth flows, you would make one change in your backend, and one change in your frontend. Here's how it would look: ### 1. Backend: db.auth.createToken Create a new `sign-in` endpoint in your backend. This endpoint will use `db.auth.createToken` to generate an authentication token for the user. ```typescript app.post('/sign-in', async (req, res) => { // your custom logic for signing users in // ... // on success, create and return a token const token = await db.auth.createToken({ email }); return res.status(200).send({ token }); }); ``` `db.auth.createToken` accepts either an email or a UUID. For the UUID variant: ```typescript const token = await db.auth.createToken({ id }); ``` If a user with the provided id or email does not exist, `db.auth.createToken` will create the user for you. ### 2. Frontend: db.auth.signInWithToken Once your frontend calls your `sign-in` endpoint, it can then use the generated token and sign a user in with `db.auth.signInWithToken`. Here's a full example: ```typescript import React, { useState } from 'react'; import { init } from '@interface-db/react'; // Instant app const APP_ID = "__APP_ID__"; const db = init({ appId: APP_ID }); async function customSignIn( email: string, password: string ): Promise<{ token: string }> { const response = await fetch('your-website.com/api/sign-in', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, password }), }); const data = await response.json(); return data; } function App() { const { isLoading, user, error } = db.useAuth(); if (isLoading) { return))} {canLoadNextPage && ( )} Loading...; } if (error) { returnUh oh! {error.message}; } if (user) { returnHello {user.email}!; } return; } function Login() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const handleEmailChange = (event: React.ChangeEvent ) => { setEmail(event.target.value); }; const handlePasswordChange = (event: React.ChangeEvent ) => { setPassword(event.target.value); }; const handleSignIn = async () => { const data = await customSignIn(email, password); // initiate your custom sign in flow db.auth.signInWithToken(data.token); // sign in with the token on success }; return ( ); } ``` ## Custom magic codes We support a [magic code flow](/docs/auth) out of the box. However, if you'd like to use your own email provider to send the code, you can do this with the `db.auth.generateMagicCode` function: ```typescript app.post('/custom-send-magic-code', async (req, res) => { const { code } = await db.auth.generateMagicCode(req.body.email); // Now you can use your email provider to send magic codes await sendMyCustomMagicCodeEmail(req.body.email, code); return res.status(200).send({ ok: true }); }); ``` You can also use Instant's default email provider to send a magic code with `db.auth.sendMagicCode`: ```typescript // You can trigger a magic code email in your backend with `sendMagicCode` const { code } = await db.auth.sendMagicCode(req.body.email); ``` Similarly, you can verify a magic code with `db.auth.checkMagicCode`. You can pass `extraFields` to set custom `$users` properties when the user is first created. The response includes a `created` boolean so you can distinguish new users from returning ones. ```typescript const { user, created } = await db.auth.checkMagicCode( req.body.email, req.body.code, { extraFields: { nickname: req.body.nickname } }, ); const token = user.refresh_token; if (created) { // first-time signup } ``` ## Authenticated Endpoints You can also use the Admin SDK to authenticate users in your custom endpoints. This would have two steps: ### 1. Frontend: user.refresh_token In your frontend, the `user` object has a `refresh_token` property. You can pass this token to your endpoint: ```javascript // client import { init } from '@interface-db/react'; const db = init(/* ... */) function App() { const { user } = db.useAuth(); // call your api with `user.refresh_token` function onClick() { myAPI.customEndpoint(user.refresh_token, ...); } } ``` ### 2. Backend: auth.verifyToken You can then use `auth.verifyToken` to verify the `refresh_token` that was passed in. ```typescript app.post('/custom_endpoint', async (req, res) => { // verify the token this user passed in const user = await db.auth.verifyToken(req.headers['token']); if (!user) { return res.status(401).send('Uh oh, you are not authenticated'); } // ... }); ``` ### Syncing Auth Sometimes you want to get the logged in user in the backend without needing to explicitly pass in the refresh token from the frontend. Here's how you can do it. Instant provides a `createInstantRouteHandler` function that generates a web standard endpoint that can be used to sync the refresh token to a cookie that your server can read. To use it in Next.js: ```typescript // src/app/api/instant/route.ts import { createInstantRouteHandler } from '@interface-db/react/nextjs'; export const { POST } = createInstantRouteHandler({ appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!, }); ``` The GET and POST functions accept a [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and return a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Request), so they should be able to be used in any framework. Then, provide your mounted API URL to the `init` function. ```typescript import { init } from '@interface-db/react'; export const db = init({ appId: process.env.NEXT_PUBLIC_INSTANT_APP_ID!, firstPartyPath: '/api/instant', // the endpoint that you registered the route handler at. schema, useDateObjects: true, }); ``` If using Next.js, you can call getUnverifiedUserFromInstantCookie with the app ID to retrieve the user in any server component or route handler. ```typescript import { getUnverifiedUserFromInstantCookie } from "@interface-db/react/nextjs"; // This is a server component! export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { // Get the instant user from the cookie const user = await getUnverifiedUserFromInstantCookie(process.env.NEXT_PUBLIC_INSTANT_APP_ID!); return ({children} ); } ``` # Patterns Common patterns for working with InstantDB. Below are some common patterns for working with InstantDB. We'll add more patterns over time, and if you have a pattern you'd like to share, please feel free to submit a PR for this page. ## You can expose your app id to the client. As with Firebase, the app id is a unique identifier for your application. If you want to secure your data, you'll want to add [permissions](/docs/permissions) for the app. ## Restrict creating new attributes. When you're ready to lock down your schema, you can restrict creating a new attribute by adding this to your app's [permissions](/dash?t=perms) ```json { "attrs": { "allow": { "$default": "false" } } } ``` This will prevent any new attributes from being created. ## Attribute level permissions When you query a namespace, it will return all the attributes for an entity. You can use the [`fields`](/docs/instaql#select-fields) clause to restrict which attributes are returned from the server, but this will not prevent a client from doing another query to get the full entity. You can set special permission rules for [`fields`](/docs/instaql#select-fields) to make sure only certain attributes are visible to certain users. ## Find entities with no links. If you want to find entities that have no links, you can use the `$isNull` query filter. For example, if you want to find all posts that are not linked to an author, you can do the following: ```javascript const { isLoading, error, data } = db.useQuery({ posts: { $: { where: { 'author.id': { $isNull: true, }, }, }, }, }); ``` ## Setting limits via permissions. If you want to limit the number of entities a user can create, you can do so via permissions. Here's an example of limiting a user to creating at most 2 todos. First the [schema](/docs/modeling-data): ```typescript // instant.schema.ts // Here we define users, todos, and a link between them. import { i } from '@interface-db/core'; const _schema = i.schema({ entities: { $users: i.entity({ email: i.string().unique().indexed(), }), todos: i.entity({ label: i.string(), }), }, links: { userTodos: { forward: { on: 'todos', has: 'one', label: 'owner', }, reverse: { on: '$users', has: 'many', label: 'ownedTodos', }, }, }, }); // This helps TypeScript display nicer IntelliSense type _AppSchema = typeof _schema; interface AppSchema extends _AppSchema {} const schema: AppSchema = _schema; export type { AppSchema }; export default schema; ``` Then the [permissions](/docs/permissions): ```typescript import type { InstantRules } from '@interface-db/react'; // instant.perms.ts // And now we reference the `owner` link for todos to check the number // of todos a user has created. // (Note): Make sure the `owner` link is already defined in the schema // before you reference it in the permissions. const rules = { todos: { allow: { create: "size(data.ref('owner.ownedTodos.id')) <= 2", }, }, } satisfies InstantRules; export default rules; ``` ## Listen to InstantDB connection status. Sometimes you want to let clients know when they are connected to or disconnected from the DB. You can use `db.subscribeConnectionStatus` in vanilla JS or `db.useConnectionStatus` in React to listen to connection changes. ```javascript // Vanilla JS const unsub = db.subscribeConnectionStatus((status) => { const statusMap = { connecting: 'authenticating', opened: 'authenticating', authenticated: 'connected', closed: 'closed', errored: 'errored', }; const connectionState = statusMap[status] || 'unexpected state'; console.log('Connection status:', connectionState); }); // React/React Native function App() { const statusMap = { connecting: 'authenticating', opened: 'authenticating', authenticated: 'connected', closed: 'closed', errored: 'errored', }; const status = db.useConnectionStatus(); const connectionState = statusMap[status] || 'unexpected state'; returnConnection state: {connectionState}; } ``` ## Using Instant via CDN If you have a plain html page or avoid using a build step, you can use InstantDB via a CDN through [unpkg](https://www.unpkg.com/@interface-db/core/). ```jsx ``` ## Making Local ids Sometimes you need an identifier that stays the same between refreshes. A "local id" of sorts. Local ids are especially useful for features like "guest" mode. You need an identifier for the user who is accessing the service, but they haven't signed up yet. Well, you can use a `localId` for that. To generate one, use `db.getLocalId`: ```js import { init } from '@interface-db/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); const id = await db.getLocalId('guest'); console.log(id, 'stays the same even if you refresh'); ``` Or a handy hook if you're inside React: ```js import { init } from '@interface-db/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { const id = db.useLocalId('guest'); if (!id) return; console.log(id, 'stays the same even if you refresh'); } ``` Note: passing in different arguments will produce different ids: ```js const id1 = db.useLocalId('device'); const id2 = db.useLocalId('session'); console.log( id1, id2, 'are different. But each will stay the same even if you refresh', ); ``` Once you have an ID, you can pass it around in your transactions and queries and use it in [ruleParams](/docs/permissions#rule-params). ## Making admin queries work with Next.js caching Next.js caches fetch requests and lets you revalidate them. [`adminDB.query`](/docs/backend#query) uses fetch under the hood, so Next.js caching will work by default. If you want to finely control how the query caches, you can pass in the same kind of [fetch options](https://nextjs.org/docs/app/building-your-application/caching#fetch) for Next.js. For example, to revalidate a query every hour: ```js await adminDB.query( { goals: {} }, { fetchOpts: { next: { revalidate: 3600 }, }, }, ); ``` Or to set a specific tag: ```js await adminDB.query( { goals: {} }, { fetchOpts: { next: { tags: ['goals:all'] }, }, }, ); ``` ## Composite keys Sometimes an item is unique by two or more attributes. For example, consider a `location`: it's unique by `latitude` _and_ `longitude`. How can you enforce this uniqueness in Instant? We don't have composite keys built-in, but you can manage them by creating a composite column. For example, you can make sure `locations` are unique by adding a `latLong` column: ```js import { i } from '@interface-db/core'; const _schema = i.schema({ entities: { // ... locations: i.entity({ latitude: i.number().indexed(), longitude: i.number().indexed(), latLong: i.string().unique() // <-- our composite column }), }, ``` We can then set `latLong` in our updates: ```js function createLocation({ latitude, longitude }) { db.transact( db.tx.locations[id()].update({ latitude, longitude, latLong: `${latitude}_${longitude}`, }), ); } ``` Now, any locations with the same latitude and longitude will throw a uniqueness error. To make sure that `latLong` _always_ matches `latitude` and `longitude`, you can add a rule in your permissions: ```js const rules = { locations: { allow: { create: "(data.latitude + '_' + data.longitude) == data.latLong", update: "(newData.latitude + '_' + newData.longitude) == newData.latLong", }, }, }; ``` ## Managing local vs production apps You may want to have separate Instant apps for local development and production. The way to do this right now is to have two separate apps, one for local and one for production. ```javascript // lib/db.ts import { init } from '@interface-db/react'; const APP_ID = process.env.NEXT_PUBLIC_INSTANT_APP_ID; export const db = init({ appId: APP_ID }); ``` Then in your environment files: ```bash # .env.local NEXT_PUBLIC_INSTANT_APP_ID=your-local-app-id # .env.production NEXT_PUBLIC_INSTANT_APP_ID=your-production-app-id ``` When developing new features that require schema or permission changes, you can follow this workflow: 1. **Push schema/perms changes locally first** ```bash npx @interface-db/cli push --app your-local-app-id ``` 2. **Test code changes** - Verify your new code works as expected with your changes in your local environment and app. 3. **Push changes to production** ```bash npx @interface-db/cli push --app your-production-app-id ``` 4. **Deploy code changes to prod!** - And that should be it! ## Dealing with timeouts Complicated queries or large transactions may fail due to timeouts. Right now we have a hard limit of 5 seconds for both queries and transactions. We do this to ensure realtime updates don't suffer from long-running operations. To get a sense for how long a query or transaction takes, you can use the `Sandbox` tab in the dashboard. Using `await db.query` or `await db.transact` will show you the time it took to run the operation in the console. Queries and transactions in the sandbox can run for up to 30 seconds. Once you have a sense of how long your queries and transactions take, you can iteratively optimize them. For example, you can use pagination or add indexes to speed up queries, or break up large transactions into smaller ones. ## Setting default values when making an attribute required When converting an optional attribute to a required one, you'll need to update existing entities with null values. For example, if you try to make `done` required on `todos` but some records have null values: ``` INVALID DATA adding required constraint to todos.done. First few examples: namespace | id | done ----------------|---------------------------------------|------ todos | 13a09c93-cee0-4e21-9e41-c7e08edd1649 | null todos | 043eec60-d971-4689-8e4a-099e82c251a2 | null ``` The solution is to run an admin script to backfill the null values before pushing the schema change. ```typescript // scripts/backfill-todos.ts import { init, tx } from '@interface-db/admin'; const db = init({ appId: process.env.INSTANT_APP_ID!, adminToken: process.env.INSTANT_ADMIN_TOKEN!, }); async function backfillDefaults() { // Query for entities with null/missing done const { data } = await db.query({ todos: { $: { where: { done: { $isNull: true }, }, }, }, }); // Update them with a default value const updates = data.todos.map((todo) => tx.todos[todo.id].update({ done: false }), ); if (updates.length > 0) { await db.transact(updates); console.log(`Backfilled ${updates.length} records`); } else { console.log('No records to backfill'); } } backfillDefaults(); ``` Then run the script before pushing your schema: ```bash npx tsx scripts/backfill-todos.ts && npx @interface-db/cli push ``` This pattern gives you full control over setting default values. You can set static values as shown above, or use more complex logic to determine the default based on other attributes. # Auth Instant supports magic code, OAuth, Clerk, and custom auth. Instant comes with support for auth. We currently offer [Magic Codes](/docs/auth/magic-codes), [Guest Auth](/docs/auth/guest-auth), [Google OAuth](/docs/auth/google-oauth), [Sign In with Apple](/docs/auth/apple), [GitHub OAuth](/docs/auth/github-oauth), [LinkedIn OAuth](/docs/auth/linkedin-oauth), and [Clerk](/docs/auth/clerk). If you want to build your own flow, you can use the [Admin SDK](/docs/backend#custom-auth). ## Auth Overview To get the current user in your application, you can use the `db.useUser` hook. ```tsx import db from '../lib/db'; function Dashboard() { const user = db.useUser(); returnSigned in as: {user.email}; } ``` The `useUser` hook will throw an error if it is accessed while the user is not logged in, so it should be gated behind ``. ```tsx import db from '../lib/db'; export default function App() { return ( ); } function Dashboard() { // This component will only render if the user is signed in // so it's safe to call useUser here! const user = db.useUser(); return Log in to see the dashboard!Signed in as: {user.email}; } ``` Use `` and ` ` to conditionally render components based on the user's authentication state. You can then use `db.auth.signOut()` to sign a user out. ```tsx import db from '../lib/db'; // ... Same app component from above function Dashboard() { const user = db.useUser(); return ( ); } ``` Putting it all together, you can conditionally render a login and dashboard component like so: ```tsx import db from '../lib/db'; export default function App() { return (Signed in as: {user.email}); } function Dashboard() { // This component will only render if the user is signed in // so it's safe to call useUser here! const user = db.useUser(); return Signed in as: {user.email}; } function Login() { // Implement a login flow here via magic codes, OAuth, Clerk, etc. } ``` To implement a login flow, use one of the authentication method guides below. ## Authentication Methods - [Magic Codes](/docs/auth/magic-codes): Send login codes to your users via email. Removes the need for passwords. - [Guest Auth](/docs/auth/guest-auth): Allow your users to try your app before they sign up. - [Google OAuth](/docs/auth/google-oauth): We provide flows for Web and React Native to enable Google OAuth for your app. - [Sign In with Apple](/docs/auth/apple): Sign In to native apps with Apple ID. - [GitHub OAuth](/docs/auth/github-oauth): Log in with GitHub on both Web and React Native. - [LinkedIn OAuth](/docs/auth/linkedin-oauth): Log in with LinkedIn on both Web and React Native. - [Clerk](/docs/auth/clerk): Integrate Clerk's auth flow with Instant. - [Firebase Auth](/docs/auth/firebase): Integrate Firebase's auth flow with Instant. - [Custom Auth](/docs/backend#custom-auth): Integrate your own auth flow with the Admin SDK. ## Additional Auth APIs Sometimes you need finer control over the state of auth in your application. In those cases, you can use some of the lower-level APIs. ### useAuth Use `useAuth` to fetch the current user. In this example we guard against loading our `Main` component until a user is logged in: ```javascript function App() { const { isLoading, user, error } = db.useAuth(); if (isLoading) { return null; // or a loading spinner } if (error) { returnUh oh! {error.message}; } if (user) { return ; } return; } ``` ### Get auth For scenarios where you want to know the current auth state without subscribing to changes, you can use `getAuth`. ```javascript const user = await db.getAuth(); console.log('logged in as', user.email); ``` ### Setting custom properties at signup If you want to set custom properties on `$users` at signup time, see [Setting properties at signup](/docs/users#setting-properties-at-signup). # Magic Code Auth How to add magic code auth to your Instant app. Instant supports a "magic-code" flow for auth. Users provide their email, we send them a login code on your behalf, and they authenticate with your app. Choose the platform you're building for to see a full example. - **Web**: For Next.js or other React frameworks - **Mobile**: For Expo and React Native - **Vanilla JS**: For non-React-based frameworks ## Full Magic Code Example Here's a full example of magic code auth in a React app. Open up your `app/page.tsx` file, and replace the entirety of it with the following code: ```tsx 'use client'; import React, { useState } from 'react'; import { init } from '@interface-db/react'; const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { return ( <> > ); } function Main() { const user = db.useUser(); return ( ); } function Login() { const [sentEmail, setSentEmail] = useState(''); return (Hello {user.email}!
); } function EmailStep({ onSendEmail }: { onSendEmail: (email: string) => void }) { const inputRef = React.useRef{!sentEmail ? () : ( )} (null); const handleSubmit = (e: React.FormEvent ) => { e.preventDefault(); const inputEl = inputRef.current!; const email = inputEl.value; onSendEmail(email); db.auth.sendMagicCode({ email }).catch((err) => { alert('Uh oh :' + err.body?.message); onSendEmail(''); }); }; return ( ); } function CodeStep({ sentEmail }: { sentEmail: string }) { const inputRef = React.useRef (null); const handleSubmit = (e: React.FormEvent ) => { e.preventDefault(); const inputEl = inputRef.current!; const code = inputEl.value; db.auth.signInWithMagicCode({ email: sentEmail, code }).catch((err) => { inputEl.value = ''; alert('Uh oh :' + err.body?.message); }); }; return ( ); } export default App; ``` Here's a full example of magic code auth in a React Native app. Open up your `app/index.tsx` file, and replace the entirety of it with the following code: ```tsx import React, { useState } from 'react'; import { View, Text, TextInput, Button, Alert, StyleSheet } from 'react-native'; import { init } from '@interface-db/react-native'; const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { const { isLoading, user, error } = db.useAuth(); if (isLoading) { return Loading... ; } if (error) { returnUh oh! {error.message} ; } if (user) { return ; } return; } function Main() { const user = db.useUser(); return ( ); } function Login() { const [sentEmail, setSentEmail] = useState(''); const [email, setEmail] = useState(''); const [code, setCode] = useState(''); if (!sentEmail) { return ( Hello {user.email}! ); } return ( Let's log you in! ); } const styles = StyleSheet.create({ container: { padding: 16, }, header: { fontSize: 24, fontWeight: 'bold', }, title: { fontSize: 20, fontWeight: 'bold', }, input: { borderWidth: 1, borderColor: '#ccc', padding: 8, marginVertical: 8, }, }); export default App; ``` Here's a full example of magic code auth with vanilla JavaScript. Open up your `src/main.ts` file, and replace the entirety of it with the following code: ```typescript import { init, type User } from '@interface-db/core'; const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); const app = document.getElementById('app')!; let sentEmail = ''; function renderApp(user: User | undefined) { if (user) { renderMain(user); } else { renderLogin(); } } function renderMain(user: User) { app.innerHTML = ` Enter your code We sent an email to {sentEmail}. Check your email, and enter the code you see. `; document.getElementById('sign-out')!.addEventListener('click', () => { db.auth.signOut(); }); } function renderLogin() { if (!sentEmail) { renderEmailStep(); } else { renderCodeStep(); } } function renderEmailStep() { app.innerHTML = `Hello ${user.email}!
`; document.getElementById('email-form')!.addEventListener('submit', (e) => { e.preventDefault(); const email = (document.getElementById('email-input') as HTMLInputElement) .value; sentEmail = email; renderLogin(); db.auth.sendMagicCode({ email }).catch((err) => { alert('Uh oh: ' + err.body?.message); sentEmail = ''; renderLogin(); }); }); } function renderCodeStep() { app.innerHTML = `Let's log you in
Enter your email, and we'll send you a verification code. We'll create an account for you too if you don't already have one.
`; document.getElementById('code-form')!.addEventListener('submit', (e) => { e.preventDefault(); const codeInput = document.getElementById('code-input') as HTMLInputElement; const code = codeInput.value; db.auth.signInWithMagicCode({ email: sentEmail, code }).catch((err) => { alert('Uh oh: ' + err.body?.message); codeInput.value = ''; }); }); } db.subscribeAuth((auth) => { renderApp(auth.user); }); ``` Make sure you have a `` element in your HTML. --- **Let's dig deeper.** We created a login flow to handle magic code auth. Of note are `auth.sendMagicCode` and `auth.signInWithMagicCode`. On successful validation, Instant's backend will return a user object with a refresh token. The client SDK will then restart the WebSocket connection with Instant's sync layer and provide the refresh token. When doing queries or transactions, the refresh token will be used to hydrate `auth` on the backend during permission checks. On the client, auth will now be populated with a `user` -- huzzah! ## Send a Magic Code ```javascript db.auth.sendMagicCode({ email }).catch((err) => { alert('Uh oh :' + err.body?.message); onSendEmail(''); }); ``` Use `auth.sendMagicCode` to generate a magic code on Instant's backend and email it to the user. ## Sign in with Magic Code ```javascript db.auth.signInWithMagicCode({ email: sentEmail, code }).catch((err) => { inputEl.value = ''; alert('Uh oh :' + err.body?.message); }); ``` You can then use `auth.signInWithMagicCode` to authenticate the user with the magic code they provided. ## Setting properties at signup Pass `extraFields` to `signInWithMagicCode` to set custom `$users` properties when a user is first created. Fields are only written on first signup; returning users are unaffected. ```javascript const { user, created } = await db.auth.signInWithMagicCode({ email: sentEmail, code, extraFields: { nickname: 'nezaj' }, }); if (created) { // Scaffold default data for new users db.transact([ db.tx.settings[id()].update({ theme: 'light' }).link({ user: user.id }), ]); } ``` The fields must be defined as optional attributes on `$users` in your schema. A `create` rule on `$users` is also required. See [Setting properties at signup](/docs/users#setting-properties-at-signup) for the full guide. ## Test users If you need to create a user with a pre-defined code for app store review or testing, you can assign a magic code to an email from the Auth page of the Dashboard. When a test user signs in, they can use the static code instead of receiving the code in an email. The static code will never expire and will always be valid until it is deleted from the dashboard. # Guest Auth Let your users try your app before creating an account Instant supports guest authentication. This allows your users to try your app before signing up and ensures they can keep all their data when they decide to create a full account with their email. ## Signing in as a Guest Use `db.auth.signInAsGuest()` to create a new guest user. This will create a new guest user with an ID but no email address. You can set custom `$users` properties when creating the guest by passing `extraFields`: ```javascript db.auth.signInAsGuest({ extraFields: { nickname: 'nezaj' }, }); ``` The fields must be optional attributes on `$users`, and `$users` must have a `create` rule. See [Setting properties at signup](/docs/users#setting-properties-at-signup) for details. ```tsx 'use client'; import React, { useState } from 'react'; import { init, User } from '@interface-db/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { return ( <>Enter your code
We sent an email to ${sentEmail}. Check your email, and paste the code you see.
> ); } function Main() { const user = db.useUser(); return ( ); } function Login() { return (Hello {user.isGuest ? 'Guest' : user.email}!
); } export default App; ``` ## Upgrading to a full user When a guest user is ready to create a permanent account, you can use any of Instant's sign-in methods. The guest user will be automatically upgraded to a full user. Here is a full example using magic code auth: ```tsx 'use client'; import React, { useState } from 'react'; import { init, User } from '@interface-db/react'; // Instant app const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { return ( <>> ); } function Main() { const user: User = db.useUser(); return ( ); } function Upgrade() { const [sentEmail, setSentEmail] = useState(''); return (Hello {user.isGuest ? 'Guest' : user.email}!
{user.isGuest &&} ); } function Login() { const [sentEmail, setSentEmail] = useState(''); return ({!sentEmail ? () : ( )} ); } function EmailStep({ onSendEmail }: { onSendEmail: (email: string) => void }) { const inputRef = React.useRef{!sentEmail ? () : ( )} (null); const handleSubmit = (e: React.FormEvent ) => { e.preventDefault(); const inputEl = inputRef.current!; const email = inputEl.value; onSendEmail(email); db.auth.sendMagicCode({ email }).catch((err) => { alert('Uh oh :' + err.body?.message); onSendEmail(''); }); }; return ( ); } function CodeStep({ sentEmail }: { sentEmail: string }) { const inputRef = React.useRef (null); const handleSubmit = (e: React.FormEvent ) => { e.preventDefault(); const inputEl = inputRef.current!; const code = inputEl.value; db.auth.signInWithMagicCode({ email: sentEmail, code }).catch((err) => { inputEl.value = ''; alert('Uh oh :' + err.body?.message); }); }; return ( ); } export default App; ``` ## Handling conflicting users If a guest user signs up with an email that is not already associated with an existing account, their user `id` remains the same, and they retain access to all data they created as a guest. However, if a user with that email already exists, the guest user's data may need to be merged. You can fetch the list of guest users for a user with this query: ```ts const query = { $users: { $: { where: { linkedPrimaryUser: user.id, }, }, }, }; ``` The linked guest users are also available on the user itself: ```ts const query = { $users: { $: { where: { id: user.id, }, linkedGuestUsers: {}, }, }, }; ``` You can then query for the data owned by those guest users and transfer it to the primary account. The specific implementation will depend on your application's data model. To enable your user to access the data stored by the guest users, you can update your rules to grant access: ```diff { "todos": { "bind": [ "isOwner", "data.owner == auth.id", + "isGuestOwner", "data.owner in auth.ref('$user.linkedGuestUsers.id')" ], "allow": { - "view": "isOwner", + "view": "isOwner || isGuestOwner", "create": "isOwner", - "update": "isOwner", + "update": "isOwner || isGuestOwner", "delete": "isOwner" } } } ``` Here's an example of how you might transfer `todos` from a guest account to the primary user: ```ts function App() { const user = db.useUser(); useEffect(() => { if (user.isGuest) return; const transferGuestData = async () => { // Get the linked guest user const { data: { $users }, } = await db.queryOnce({ $users: { $: { where: { linkedPrimaryUser: user.id }, limit: 1, order: { serverCreatedAt: desc }, }, }, }); const guestId = $users[0]?.id; if (!guestId) return; // Get the data for the guest user const { data: { todos }, } = await db.queryOnce({ todos: { $: { where: { owner: guestId }, }, }, }); if (!todos.length) return; // Update owner on all of the guest's todo entities const txes = todos.map((todo) => db.tx.todos[todo.id].update({ owner: user.id }), ); await db.transact(txes); }; transferGuestData(); }, [user]); } ``` # Google OAuth How to add Google OAuth to your Instant app. Instant supports logging users in with their Google account. There are a few ways to do this: it depends on whether you are building for web or React Native. Choose the option that sounds best to you, and the rest of the document will show you how to add Sign in with Google to your app. **Building for Web?** - **Google Button**: Use Google's pre-styled button to sign in. This is what Google recommends. - **Web Redirect**: Easier to integrate but requires a redirect rule to render your custom app name. **Building for React Native?** - **Native Auth**: Use react-native-google-signin to integrate with the native Google iOS and Android flows and render your custom app name in the consent screen. - **Expo Web Auth**: Use Expo's auth session to integrate browser-based sign-in. Easier to implement, but doesn't let you render your custom app name. ## Overview There are three main steps: 1. **Google Console**: Set up your consent screen and create an OAuth client. 2. **Instant Dashboard**: Connect your OAuth client to Instant 3. **Your app**: Add some code to log in with Google! Let's dive deeper into each step: ## Quick start: developer credentials If you're testing Google sign-in locally, we have a fast option for you. Instant provides developer credentials that you can use for local development. You don't need to go to the Google Cloud Console, and you can log in within minutes. **From the dashboard** From the [Auth](/dash?s=main&t=auth) tab on the Instant dashboard: - Click "Set up Google" - Toggle "Use dev credentials" - Give your client a name (e.g. `google-web`) - Click "Add Client" **From the terminal** ```shell npx @interface-db/cli@latest auth client add \ --type google --app-type web --name google-web --dev-credentials ``` **And you're done. You can go [straight to code](#3-add-some-code).** Developer credentials are meant only for local development. You're limited to 100 sign-ups. Once you're ready for production, follow the steps below. ## 1. Set up your consent screen and create an OAuth client Head on over to [Google Console](https://console.cloud.google.com/apis/credentials). You should be in the "Credentials" section. **Configure your Google OAuth consent screen** - Click "CONFIGURE CONSENT SCREEN." If you already have a consent screen, you can skip to the next step. - Select "External" and click "CREATE". - Add your app's name, a support email, and developer contact information. Click "Save and continue". - No need to add scopes or test users. Click "Save and continue" for the next screens. Until you reach the "Summary" screen, click "Back to dashboard". **Create an OAuth client for Google** - From Google Console, click "+ CREATE CREDENTIALS" - Select "OAuth client ID" - Select "Web application" as the application type. - Add `https://api.interfacedb.com/runtime/oauth/callback` as an Authorized redirect URI. - If you're testing from localhost, **add both `http://localhost`** and `http://localhost:3000` to "Authorized JavaScript origins", replacing `3000` with the port you use. - For production, add your website's domain. And with that you have your OAuth client! Save your Client ID and your Client Secret -- you'll need them for the next step! For native auth, each platform needs an OAuth Client. If you support both iOS and Android, for example, you'll create two clients. Here are the steps: - From Google Console, click "+ CREATE CREDENTIALS" - Select "OAuth client ID" - Select "iOS" or "Android" as the application type. - Fill in your bundle information. And with that you're ready! Save your Client IDs -- you'll need them for the next step! ## 2. Connect your OAuth client to Instant **Add your OAuth Client on Instant** **From the dashboard** From the [Auth](/dash?s=main&t=auth) tab on the Instant dashboard: - Click "Set up Google" - Enter your "Client ID" - Enter your "Client Secret" - Check "I added the redirect to Google" (make sure you actually did this!) - Click "Add Client" **From the terminal** ```shell npx @interface-db/cli@latest auth client add \ --type google --app-type web --name google-web \ --client-id --client-secret ``` **Register your website with Instant** If you're testing from localhost, add `http://localhost:3000` (replacing `3000` with the port you use). For production, add your website's domain. **From the dashboard** From the [Auth](/dash?s=main&t=auth) tab on the Instant dashboard, add your URL to the **Redirect Origins** list. **From the terminal** ```shell npx @interface-db/cli@latest auth origin add --type website --url ``` **Add your OAuth Client on Instant** For each OAuth Client you created, add it to Instant: **From the dashboard** From the [Auth](/dash?s=main&t=auth) tab on the Instant dashboard: - Click "Set up Google" - Enter your "Client ID" - Click "Add Client" **From the terminal** ```shell npx @interface-db/cli@latest auth client add \ --type google --app-type ios --name google-ios --client-id npx @interface-db/cli@latest auth client add \ --type google --app-type android --name google-android --client-id ``` ## 3. Add some code! **Method: Google Sign in Button for Web** We'll use [Google's pre-built Sign in Button](https://developers.google.com/identity/gsi/web/guides/overview). The benefit of using Google's button is that you can display your app's name in the consent screen. There are two steps to the code: 1. Use the Sign in Button to auth with Google and get an `idToken` 2. Pass the token on to `db.auth.signInWithIdToken`, and you are logged in! Let's do that. **Using React** If you're using React, the easiest way to include the Sign in Button is through the [@react-oauth/google](https://github.com/MomenSherif/react-oauth) package: ```shell npm install @react-oauth/google ``` Once you install it, include the button, and use `db.auth.signInWithIdToken` to complete sign in. Here's a full example: ```jsx 'use client'; import React, { useState } from 'react'; import { init } from '@interface-db/react'; import { GoogleOAuthProvider, GoogleLogin } from '@react-oauth/google'; const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); // e.g. 89602129-cuf0j.apps.googleusercontent.com const GOOGLE_CLIENT_ID = 'REPLACE_ME'; // Use the google client name in the Instant dashboard auth tab const GOOGLE_CLIENT_NAME = 'REPLACE_ME'; function App() { return ( <> > ); } function UserInfo() { const user = db.useUser(); return Hello {user.email}!
; } function Login() { const [nonce] = useState(crypto.randomUUID()); return (); } ``` **Not using React?** If you're not using React or prefer to embed the button yourself, refer to [Google's docs](https://developers.google.com/identity/gsi/web/guides/overview) on how to create the button and load their client library. When creating your button, make sure to set the `data-ux_mode="popup"`. Your `data-callback` function should look like: ```jsx async function handleSignInWithGoogle(response) { await db.auth.signInWithIdToken({ // Use the google client name in the Instant dashboard auth tab clientName: 'REPLACE_ME', idToken: response.credential, // make sure this is the same nonce you set in data-nonce nonce: 'REPLACE_ME', }); } ``` **Method: Web Redirect** If you don't want to use the Google-styled buttons, you can use the redirect flow instead. Create an authorization URL via `db.auth.createAuthorizationURL` and then use the URL to create a link. Here's a full example: ```jsx 'use client'; import React, { useState } from 'react'; import { init } from '@interface-db/react'; const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); const url = db.auth.createAuthorizationURL({ // Use the google client name in the Instant dashboard auth tab clientName: 'REPLACE_ME', redirectURL: window.location.href, }); import React from 'react'; export default function App() { return ( <> alert('Login failed')} onSuccess={({ credential }) => { db.auth .signInWithIdToken({ clientName: GOOGLE_CLIENT_NAME, idToken: credential, // Make sure this is the same nonce you passed as a prop // to the GoogleLogin button nonce, }) .catch((err) => { alert('Uh oh: ' + err.body?.message); }); }} /> > ); } function UserInfo() { const user = db.useUser(); return Hello {user.email}!
; } function Login() { return Log in with Google; } ``` When your users click on the link, they'll be redirected to Google to start the OAuth flow and then back to your site. Instant will automatically log them in to your app when they are redirected! **Method: Expo Web Auth** Instant comes with support for Expo's AuthSession library. To use it, you need to: 1. Set up AuthSession 2. Register your app with Instant 3. Use AuthSession to log in with Google! Let's do that. **Set up AuthSession** If you haven't already, follow the AuthSession [installation instructions](https://docs.expo.dev/versions/latest/sdk/auth-session/) from the Expo docs. Next, add the following dependencies: ```shell npx expo install expo-auth-session expo-crypto ``` Update your app.json with your scheme: ```json { "expo": { "scheme": "mycoolredirect" } } ``` **Register your app with Instant** Now that you have your App Scheme, it's time to tell Instant about it. For development with Expo, add `exp://` and your scheme (e.g. `mycoolredirect://`) as redirect origins. **From the dashboard** From the [Auth](/dash?s=main&t=auth) tab on the Instant dashboard, add a redirect origin of type "App scheme". **From the terminal** ```shell npx @interface-db/cli@latest auth origin add --type custom-scheme --scheme exp:// npx @interface-db/cli@latest auth origin add --type custom-scheme --scheme mycoolredirect:// ``` **Use AuthSession to log in with Google!** And from here you're ready to add a login button to your Expo app! Here's a full example: ```jsx import { View, Text, Button, StyleSheet } from 'react-native'; import { init } from '@interface-db/react-native'; import { makeRedirectUri, useAuthRequest, useAutoDiscovery, } from 'expo-auth-session'; const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); function App() { return ( <>> ); } function UserInfo() { const user = db.useUser(); return Hello {user.email}! ; } function Login() { const discovery = useAutoDiscovery(db.auth.issuerURI()); const [request, _response, promptAsync] = useAuthRequest( { // The unique name you gave the OAuth client when you // registered it on the Instant dashboard clientId: 'YOUR_INSTANT_AUTH_CLIENT_NAME', redirectUri: makeRedirectUri(), }, discovery, ); return ( ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, }); export default App; ``` **Method: Native Auth** You can use [react-native-google-signin/google-signin](https://github.com/react-native-google-signin/google-signin) to authenticate natively on Google. There are three steps: 1. Set up google-signin on Expo 1. Use the Sign in Button to auth with Google and get an `idToken` 1. Pass the token on to `db.auth.signInWithIdToken`, and you are logged in! Let's do that. **Set up google-signin on Expo** First, let's install the package: ``` npx expo install @react-native-google-signin/google-signin ``` Then, follow the google-signin [installation instructions](https://react-native-google-signin.github.io/docs/setting-up/expo/) to set it up with Expo. **Use google-signin to log in with Google!** Now you're ready to add the Google sign-in button to your Expo app! Here's a full example: ```jsx import { View, Text, Button, StyleSheet } from 'react-native'; import { init } from '@interface-db/react-native'; import { GoogleSignin, GoogleSigninButton, } from '@react-native-google-signin/google-signin'; const APP_ID = '__APP_ID__'; const db = init({ appId: APP_ID }); GoogleSignin.configure({ // See https://react-native-google-signin.github.io/docs/original#configure iosClientId: 'YOUR_IOS_CLIENT_ID', }); function App() { return ( <>Loading...}> > ); } function UserInfo() { const user = db.useUser(); return Hello {user.email}! ; } function Login() { return ({ // 1. Sign in to Google await GoogleSignin.hasPlayServices(); const userInfo = await GoogleSignin.signIn(); const idToken = userInfo.data?.idToken; if (!idToken) { console.error('no ID token present!'); return; } // 2. Use your token to sign in to InstantDB! try { const res = await db.auth.signInWithIdToken({ // The unique name you gave the OAuth client when you // registered it on the Instant dashboard clientName: 'YOUR_INSTANT_AUTH_CLIENT_NAME', idToken, }); console.log('logged in!', res); } catch (error) { console.log('error signing in', error); } }} /> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, }); export default App; ``` # Sign In with Apple How to add Sign In with Apple to your Instant app. Instant supports Sign In with Apple on the Web and in native applications. - **Web Popup (recommended)**: Use Apple-provided popup to authenticate users - **Web Redirect**: Use redirect flow to authenticate users - **React Native**: Authenticate in a React Native app ## Step 1: Create App ID - Navigate to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list) - Select _Identifiers_ - Click _+_ - _Register a new identifier_ → Select _App IDs_ - _Select a type_ → Select _App_ - _Capabilities_ → _Sign In with Apple_ → Check - Fill in _Bundle ID_ and _Description_ - Click _Register_ ## Step 2: Create Services ID - Navigate to [Services IDs](https://developer.apple.com/account/resources/identifiers/list/serviceId) - Click _+_ - _Register a new identifier_ → Select _Services IDs_ - Fill in _Description_ and _Identifier_. You’ll need this _Identifier_ later - Click _Register_ ## Step 3: Configure Services ID (Web Popup flow) - Select the newly created Services ID - Enable _Sign In with Apple_ - Click _Configure_ - Select _Primary App ID_ from Step 1 - To _Domains_, add your app domain (e.g. `myapp.com`) - To _Return URLs_, add the URL of your app where authentication happens (e.g. `https://myapp.com/signin`) - Click _Continue_ → _Save_ ## Step 3: Configure Services ID (Web Redirect flow) - Select the newly created Services ID - Enable _Sign In with Apple_ - Click _Configure_ - Select _Primary App ID_ from Step 1 - To _Domains_, add `api.interfacedb.com` - To _Return URLs_, add `https://api.interfacedb.com/runtime/oauth/callback` - Click _Continue_ → _Save_ ## Step 3.5: Generate Private Key (Web Redirect flow only) - Navigate to [Keys](https://developer.apple.com/account/resources/authkeys/list) - Click _+_ - Fill in _Name_ and _Description_ - Check _Sign in with Apple_ - Configure → select _App ID_ from Step 1 - _Continue_ → _Register_ - Download key file ## Step 3: Configure Services ID (React Native flow) This step is not needed for Expo. ## Step 4: Register your OAuth client with Instant **From the dashboard** From the [Auth](/dash?s=main&t=auth) tab on the Instant dashboard: - Click "Add Apple Client" - Select a unique client name (`apple` by default, used in `db.auth` calls) - Fill in your "Services ID" from Step 2 - Fill in your "Team ID" from [Membership details](https://developer.apple.com/account#MembershipDetailsCard) - Fill in your "Key ID" from Step 3.5 - Fill in your "Private Key" by copying the file content from Step 3.5 - Click "Add Apple Client" **From the terminal** ```shell npx @interface-db/cli@latest auth client add \ --type apple --name apple \ --services-id \ --team-id --key-id \ --private-key-file ``` **From the dashboard** From the [Auth](/dash?s=main&t=auth) tab on the Instant dashboard: - Click "Add Apple Client" - Select a unique client name (`apple` by default, used in `db.auth` calls) - Fill in your "Services ID" from Step 2 - Click "Add Apple Client" **From the terminal** ```shell npx @interface-db/cli@latest auth client add \ --type apple --name apple --services-id ``` ## Step 4.5: Whitelist your domain in Instant (Web Redirect flow only) **From the dashboard** From the [Auth](/dash?s=main&t=auth) tab on the Instant dashboard: - Click "Redirect Origins" → "Add an origin" - Add your app's domain (e.g. `myapp.com`) **From the terminal** ```shell npx @interface-db/cli@latest auth origin add --type website --url myapp.com ``` ## Step 5: Add Sign In code to your app (Web Popup flow) Add Apple Sign In library to your app: ``` https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js ``` Initialize with `Services ID` from Step 2: ```jsx AppleID.auth.init({ clientId: ' ', scope: 'name email', redirectURI: window.location.href, }); ``` Implement `signInPopup` using `clientName` from Step 4: ```jsx async function signInPopup() { let nonce = crypto.randomUUID(); // authenticate with Apple let resp = await AppleID.auth.signIn({ nonce: nonce, usePopup: true, }); // authenticate with Instant await db.auth.signInWithIdToken({ clientName: ' ', idToken: resp.authorization.id_token, nonce: nonce, }); } ``` Add Sign In button: ```jsx ``` ## Step 5: Add Sign In code to your app (Web Popup flow) Create Sign In link using `clientName` from Step 4: ``` const authUrl = db.auth.createAuthorizationURL({ clientName: ' ', redirectURL: window.location.href, }); ``` Add a link that uses `authUrl`: ``` Sign In with Apple ``` That’s it! ## Step 5: Add Sign In code to your app (React Native flow) Instant comes with support for the [Expo AppleAuthentication library](https://docs.expo.dev/versions/latest/sdk/apple-authentication/). Add dependency: ```shell npx expo install expo-apple-authentication ``` Update `app.json` by adding: ```json { "expo": { "ios": { "usesAppleSignIn": true } } } ``` Add `exp://` as a redirect origin for development with Expo: **From the dashboard** From the [Auth](/dash?s=main&t=auth) tab on the Instant dashboard: - Click "Redirect Origins" → "Add an origin" - Add `exp://` **From the terminal** ```shell npx @interface-db/cli@latest auth origin add --type custom-scheme --scheme exp:// ``` Authenticate with Apple and then pass identityToken to Instant along with `clientName` from Step 4: ```jsx const [nonce] = useState('' + Math.random()); try { // sign in with Apple const credential = await AppleAuthentication.signInAsync({ requestedScopes: [ AppleAuthentication.AppleAuthenticationScope.FULL_NAME, AppleAuthentication.AppleAuthenticationScope.EMAIL, ], nonce: nonce, }); // pass identityToken to Instant db.auth .signInWithIdToken({ clientName: ' ', idToken: credential.identityToken, nonce: nonce, }) .catch((err) => { console.log('Error', err.body?.message, err); }); } catch (e) { if (e.code === 'ERR_REQUEST_CANCELED') { // handle that the user canceled the sign-in flow } else { // handle other errors } } ``` Sign out code: ```jsx