Introduction
Getting started with Python
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
uv add instantdb# orpip install instantdb
Python 3.10+ is required.
To scaffold a new project:
npx create-instant-app --python my-appcd my-appuv 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:
from instantdb import Instant, iddb = Instant(# You can pass these explicitly, by default they fall back to# INSTANT_APP_ID and INSTANT_APP_ADMIN_TOKEN environment variablesapp_id="__APP_ID__",admin_token="__ADMIN_TOKEN__",)# Write datagoal_id = id()db.transact(db.tx.goals[goal_id].update({"title": "Get fit"}))# Read dataresult = 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:
import asynciofrom instantdb import AsyncInstant, iddb = 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:
npx instant-cli push
#FastAPI with Pydantic
At the moment the SDK doesn't generate Python types from your schema file. Queries return 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
from fastapi import FastAPIfrom pydantic import BaseModelfrom instantdb import Instant, iddb = Instant()app = FastAPI()class TodoIn(BaseModel):text: strdone: bool = Falseclass Todo(TodoIn):id: str@app.post("/todos", response_model=Todo)def create_todo(todo: TodoIn):todo_id = id()db.transact(db.tx.todos[todo_id].update(todo.model_dump()))return Todo(id=todo_id, **todo.model_dump())
See Modeling data for the full schema reference.
#Reading data
db.query takes the same InstaQL dict shape as our JS SDK:
# Top-level fetchresult = db.query({"goals": {}, "todos": {}})# Nested childrenresult = db.query({"goals": {"todos": {}}})# Operatorsdb.query({"goals": {"$": {"where": {"title": {"$ne": "Get fit"}}}}})# Pagination + orderingdb.query({"goals": {"$": {"limit": 10, "order": {"createdAt": "desc"}}}})
#Rule params
Use rule_params keyword argument to pass values for permission rules.
db.query({"goals": {}}, rule_params={"region": "us"})
#Writing data
Mutations use the same InstaML proxy syntax.
from instantdb import Instant, id, lookupdb = 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:
db.transact(db.tx["$files"][file_id].delete())
db.tx.goals and db.tx["goals"] are interchangeable for regular namespaces.
See Writing data for the full mutation reference.
#Subscribing to queries
AsyncInstant.subscribe_query opens an SSE stream and yields payloads as the query result changes. Similar to our JS SDK:
import asynciofrom instantdb import AsyncInstantdb = 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"])breakprint("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
import asynciofrom instantdb import AsyncInstantdb = AsyncInstant()async def main():async with db.streams.write(client_id="agent-completion-123") as writer:stream_id = await writer.stream_idasync for chunk in claude_response:await writer.write(chunk)asyncio.run(main())
#Reading
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:
result = db.query({"$streams": {"$": {"where": {"clientId": "agent-completion-123"}},},})
See Streams for the full reference.
#Auth
Mirrors the JS db.auth namespace.
#Magic codes
db.auth.send_magic_code("alyssa@instantdb.com")# Or generate the code yourself for a custom email providercode = db.auth.generate_magic_code("alyssa@instantdb.com")print(code)# Verify ituser, created = db.auth.check_magic_code(email="alyssa@instantdb.com",code="123456",)
#Tokens
# Mint a token for a user (creates them if they don't exist)token = db.auth.create_token(email="alyssa@instantdb.com")# ortoken = db.auth.create_token(id="...")# Verify a refresh token (e.g. one passed in from a client)user = db.auth.verify_token(token)
#Users
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.
# As a specific userscoped = db.as_user(email="alyssa@instantdb.com")goals_as_alyssa = scoped.query({"goals": {}})# As a guestguest = db.as_user(guest=True)guest.query({"publicData": {}})# Or with a refresh tokenscoped = 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.
db = Instant(app_id="__APP_ID__") # no admin_tokenuser_db = db.as_user(token="...")user_db.query({"todos": {}})
#Storage
db.storage.upload_file accepts bytes, pathlib.Path, or a binary file-like object.
from pathlib import Path# From bytesdb.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):
db.storage.upload_file("exports/data.jsonl",stream,content_type="application/jsonl",file_size=10_000_000,)
To delete a file, use a transaction:
db.transact(db.tx["$files"][file_id].delete())
See Storage for the broader storage model.
#Rooms
Read presence data for a room:
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.
#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.
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 subscriptionsdb.webhooks.validate / fetch_payloads / process_payloadfor receiving events
#Managing subscriptions
webhook = db.webhooks.manager.create(url="https://api.example.com/instant",namespaces=["goals", "todos"],actions=["create", "update", "delete"],)# Inspectdb.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"])# Modifydb.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 framework integration:
# 1. Verify the Ed25519 signature and parse the signed bodywebhook_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 recordspayload = 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:
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
from fastapi import FastAPI, Request, HTTPExceptionfrom instantdb import AsyncInstant, InstantErrorapp = 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 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:
from instantdb import Instant, InstantAPIError, iddb = 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