An open API service indexing awesome lists of open source software.

https://github.com/viperadnan-git/supabase-orm

Lightweight async ORM on top of supabase-py with Pydantic validation. Type-safe query builder, PostgREST embeds, FastAPI-ready with per-request RLS.
https://github.com/viperadnan-git/supabase-orm

async asyncio fastapi orm postgrest pydantic python supabase

Last synced: about 15 hours ago
JSON representation

Lightweight async ORM on top of supabase-py with Pydantic validation. Type-safe query builder, PostgREST embeds, FastAPI-ready with per-request RLS.

Awesome Lists containing this project

README

          

# supabase-orm

### A lightweight, Pydantic-native ORM on top of `supabase-py` — async-first, sync mirror generated.

**[Documentation](https://supabase-orm.readthedocs.io/)** · [Install](#install) · [Quick start](#quick-start) · [Sync mode](#sync-mode) · [Contributing](#contributing)

---

## Features

- **Pydantic models are tables.** One class is your schema, validator, and query entry point — no separate DTO layer.
- **Typed query builder.** `Model.query.eq(...).gte(...)` autocompletes; typos raise at call time, not server-side.
- **Composable predicates.** `Pet.f.age >= 5` builds a `Predicate`; combine with `|` / `&` / `~`, pass to `.or_()` / `.not_()`.
- **Declarative embeds.** Annotate `Annotated[Owner, Relation(...)]` — the right `select=` string + `!inner` / FK hints are inferred.
- **Keyset iteration.** `async for pet in Pet.query.iter():` — constant-time per batch, race-safe under concurrent writes, any table size.
- **Async + sync.** Async-first for FastAPI; a byte-for-byte sync mirror at `supabase_orm.sync` is generated via `unasync`.
- **Typed RPC.** Call PostgREST functions with row validation, single-row, or scalar coercion in one line.
- **Per-request RLS.** `use_client()` with a JWT-authenticated client in a FastAPI middleware — zero leakage between concurrent requests.
- **Safe by default.** Unfiltered bulk `delete()` / `update()` raise unless you opt in explicitly.
- **Battle-tested.** 500+ mock tests for the wire contract; 80+ integration tests against real Supabase.

---

## Install

```bash
uv add supabase-orm
# or
pip install supabase-orm
```

Requires Python 3.11+, `supabase-py 2.30+`, `pydantic 2.13+`.

---

## Quick start

```python
from uuid import UUID
from typing import Annotated
from supabase_orm import SupabaseModel, Relation, lifespan

class Owner(SupabaseModel, table="owners"):
id: UUID
email: str
is_active: bool

class Pet(SupabaseModel, table="pets"):
id: UUID
name: str
species: str
adopted: bool
owner: Annotated[Owner, Relation(join="inner")]

async with lifespan(SUPABASE_URL, SUPABASE_KEY):
# Chain-style query (sequential AND)
cats = await Pet.query.eq("species", "cat").order_by("-created_at").limit(10).all()

# Typed predicates (OR / NOT / boolean composition)
rescues = await Pet.query.or_(
Pet.f.species == "cat",
(Pet.f.species == "dog") & (Pet.f.adopted == False),
).all()

# Writes
p = await Pet.create(name="Whiskers", species="cat", adopted=False)
p.name = "Mr. Whiskers"
await p.save()

# Stream every matching row, any table size
async for pet in Pet.query.eq("adopted", False).iter():
await process(pet)

# Bulk update / delete (guards block unfiltered ops)
await Pet.query.eq("adopted", False).update(adopted=True)
```

Full guide: **[https://supabase-orm.readthedocs.io](https://supabase-orm.readthedocs.io)** — models, predicates, embeds, lifecycle, RPC, extending.

---

## Sync mode

Same model classes, same chain syntax, same predicates. Switch the import and drop `await` / `async`:

```python
from supabase import create_client
from supabase_orm.sync import SupabaseModel, init, shutdown

class Pet(SupabaseModel, table="pets"):
id: UUID
name: str
species: str

init(create_client(SUPABASE_URL, SUPABASE_KEY))

cats = Pet.query.eq("species", "cat").limit(10).all()
for p in Pet.query.eq("species", "cat").iter():
process(p)

shutdown() # optional — process exit drains pools anyway
```

The sync tree is generated from the async source — no second implementation to keep in sync.

---

## Contributing

```bash
git clone https://github.com/viperadnan-git/supabase-orm
cd supabase-orm
uv sync --all-groups
uv run pytest # mock suite (always runs)
```

### Architecture

- `src/supabase_orm/_async/` is the canonical implementation.
- `src/supabase_orm/_sync/` is **generated** by `scripts/gen_sync.py` (`unasync`-based token rewrite + prose regex + skip-block directive).
- Tests mirror the same layout: `tests/_async/` is the source, `tests/_sync/` is generated.
- A pre-commit hook (`nizm`) auto-regenerates and stages the sync mirror whenever `_async/**/*.py` changes. CI also runs `python scripts/gen_sync.py --check` to fail on drift.

#### Skip-block directive

Wrap async-only test code (e.g. concurrency tests using `asyncio.gather`) so the sync mirror omits it:

```python
# gen_sync: skip-block
async def test_async_only():
...
# gen_sync: end-skip
```

### Running tests

```bash
uv run pytest # mock only (default)
uv run pytest -m integration # live Supabase
```

Integration tests need a Supabase project with the test schema. See [`tests/integration/README.md`](https://github.com/viperadnan-git/supabase-orm/blob/main/tests/integration/README.md).

### Building docs locally

```bash
uv sync --group docs
uv run mkdocs serve # http://localhost:8000
```

---

## License

Apache License 2.0 — see [LICENSE](https://github.com/viperadnan-git/supabase-orm/blob/main/LICENSE).