Database — API Reference
The supabase-database module is a thin, stateless Kotlin client over a Supabase
project’s PostgREST endpoint (/rest/v1). It gives you CRUD over your tables,
calls to stored procedures (RPC), and a typed filter DSL for building WHERE
clauses, ordering, and pagination — all mapped to a single HTTP request per call.
- Artifact:
io.github.androidpoet:supabase-database - Entry point:
createDatabaseClient(client)or theSupabaseClient.databaseaccessor.
val database = createDatabaseClient(client)
// or
val database = client.databaseEvery method returns a SupabaseResult<T> — a sealed type from
supabase-core with two cases, SupabaseResult.Success(value) and
SupabaseResult.Failure(error) where error is a typed SupabaseError. This is
not kotlin.Result. Failures (HTTP errors, transport problems, malformed
requests, decode failures) are returned, never thrown — except for the explicit
*OrThrow helpers. Pattern-match on the result, or use getOrNull(),
getOrThrow(), errorOrNull(), and map.
The interface methods return the raw response body as a String (the wire-level
escape hatch). The ergonomic, type-safe access lives in reified inline
extension functions named with a Typed/Unit/Csv/etc. suffix — these
serialize and deserialize around the string core. See Design notes
for why typed decoding is a separate name rather than an overload of select.
Client construction
createDatabaseClient
public fun createDatabaseClient(supabaseClient: SupabaseClient): DatabaseClientCreates a DatabaseClient bound to supabaseClient. The result is a thin,
stateless wrapper that forwards every call to the client’s transport, so it is
cheap to create and holds no state of its own.
supabaseClient— the configured Supabase client whose transport is used.
Returns: a DatabaseClient.
val database = createDatabaseClient(client)SupabaseClient.database
public val SupabaseClient.database: DatabaseClientAccessor property that simply calls createDatabaseClient(this). Use it for
one-off access without keeping a named reference.
val rows = client.database.selectTyped<Todo>("todos")Reads
select (interface method)
public suspend fun select(
table: String,
schema: String? = null,
columns: String = "*",
format: ResponseFormat = ResponseFormat.ROWS,
count: CountOption? = null,
stripNulls: Boolean = false,
explain: ExplainOptions? = null,
retry: Boolean = true,
headers: Map<String, String> = emptyMap(),
block: QueryBuilder.() -> Unit = {},
): SupabaseResult<String>Reads rows from table via GET /rest/v1/{table}, returning the response body as
a raw string (typically a JSON array). This is the wire-level read; prefer
selectTyped for decoded results.
table— table (or view) name.schema— target Postgres schema; sent asAccept-Profilewhen non-null, otherwise the default schema. Defaultnull.columns— the PostgRESTselect=projection (supports embedded resources and renames). Default"*".format— body shape requested viaAccept(seeResponseFormat). DefaultResponseFormat.ROWS.count— adds acount=preference so the total appears inContent-Range(seeCountOption). Defaultnull.stripNulls— omits null fields from the response. Defaultfalse.explain— when set, returns the query plan instead of data (seeExplainOptions). Defaultnull.retry— whether this read may be transparently retried by the transport. Defaulttrue.headers— extra request headers, merged with the computed ones. DefaultemptyMap().block— the query DSL: row predicates, ordering, and range.
Returns: SupabaseResult<String> — Success with the raw body, or Failure with a SupabaseError.
val json = database.select("todos", columns = "id,title") {
where { Todo.done eq false }
orderBy(Todo.createdAt, order = Order.DESC)
limit(20)
}selectTyped
public suspend inline fun <reified T> DatabaseClient.selectTyped(
table: String,
schema: String? = null,
columns: String = "*",
noinline block: QueryBuilder.() -> Unit = {},
): SupabaseResult<List<T>>The type-safe wrapper over select: decodes the JSON array body into List<T>.
table— table or view name.schema— target schema, or default. Defaultnull.columns—select=projection. Default"*".block— the query DSL.
Returns: SupabaseResult<List<T>>. Decode failures and HTTP errors both come back as Failure.
val todos: SupabaseResult<List<Todo>> = database.selectTyped("todos") {
where { Todo.done eq false }
}selectTypedOrThrow
public suspend inline fun <reified T> DatabaseClient.selectTypedOrThrow(
table: String,
schema: String? = null,
columns: String = "*",
noinline block: QueryBuilder.() -> Unit = {},
): List<T>The plain (no-SupabaseResult) form of selectTyped: returns the decoded
List<T> directly and throws SupabaseException on failure. A natural fit
for a paging fetch lambda that reports errors through its own channel.
- Parameters as for
selectTyped.
Returns: List<T> (throws on failure).
val todos: List<Todo> = database.selectTypedOrThrow("todos")selectSingleTyped
public suspend inline fun <reified T> DatabaseClient.selectSingleTyped(
table: String,
schema: String? = null,
columns: String = "*",
noinline block: QueryBuilder.() -> Unit = {},
): SupabaseResult<T>Reads the single row matching the filters and decodes it into T. Requests
format = SINGLE, so PostgREST returns HTTP 406 (a Failure with
SupabaseErrorCategory.NOT_FOUND) when zero or more than one row matches.
- Parameters as for
selectTyped.
Returns: SupabaseResult<T>.
val todo = database.selectSingleTyped<Todo>("todos") { where { Todo.id eq 7 } }selectMaybeSingleTyped
public suspend inline fun <reified T> DatabaseClient.selectMaybeSingleTyped(
table: String,
schema: String? = null,
columns: String = "*",
noinline block: QueryBuilder.() -> Unit = {},
): SupabaseResult<T?>The lenient variant of selectSingleTyped: reads at most one row, mapping a “no
rows” response (HTTP 406 / NOT_FOUND) to Success(null) instead of a failure.
Still fails if more than one row matches.
- Parameters as for
selectTyped.
Returns: SupabaseResult<T?> — Success(null) when absent.
val maybe = database.selectMaybeSingleTyped<Todo>("todos") { where { Todo.id eq 7 } }selectWithCountTyped
public suspend inline fun <reified T> DatabaseClient.selectWithCountTyped(
table: String,
schema: String? = null,
columns: String = "*",
count: CountOption = CountOption.EXACT,
noinline block: QueryBuilder.() -> Unit = {},
): SupabaseResult<PostgrestPage<T>>Selects rows and decodes them into a PostgrestPage carrying the
total count from PostgREST’s Content-Range header. Combine with limit/range
for count-aware pagination.
count— how the total is computed (seeCountOption). DefaultCountOption.EXACT.- Other parameters as for
selectTyped.
Returns: SupabaseResult<PostgrestPage<T>>.
val page = database.selectWithCountTyped<Todo>("todos") { range(0, 19) }
// page.value.rows, page.value.countselectGeoJson
public suspend fun DatabaseClient.selectGeoJson(
table: String,
schema: String? = null,
columns: String = "*",
headers: Map<String, String> = emptyMap(),
block: QueryBuilder.() -> Unit = {},
): SupabaseResult<String>Selects rows as a PostGIS GeoJSON FeatureCollection, returned as the raw JSON
string (Accept: application/geo+json). Equivalent to calling select with
format = ResponseFormat.GEOJSON.
headers— extra request headers. DefaultemptyMap().- Other parameters as for
selectTyped.
Returns: SupabaseResult<String> — the GeoJSON document.
val geo = database.selectGeoJson("stores", columns = "id,name,location")
// or: database.select("stores", format = ResponseFormat.GEOJSON)selectCsv
public suspend fun DatabaseClient.selectCsv(
table: String,
schema: String? = null,
columns: String = "*",
block: QueryBuilder.() -> Unit = {},
): SupabaseResult<String>Reads rows as CSV (Accept: text/csv), returned as the raw response string with a
header row — convenient for export/download.
- Parameters as for
selectTyped.
Returns: SupabaseResult<String> — the CSV text.
val csv = database.selectCsv("todos")selectHead
public suspend fun DatabaseClient.selectHead(
table: String,
schema: String? = null,
columns: String = "*",
count: CountOption? = null,
block: QueryBuilder.() -> Unit = {},
): SupabaseResult<Unit>Issues a HEAD request to test that a query runs or to fetch a count without
transferring rows; the empty body is discarded. A success means the query ran — it
does not indicate whether any row matched. For the parsed total use
selectCount.
count— when set (e.g.CountOption.EXACT), the total is reported inContent-Range. Defaultnull.- Other parameters as for
selectTyped.
Returns: SupabaseResult<Unit>.
val ran = database.selectHead("todos") { where { Todo.done eq false } }selectCount (interface method)
public suspend fun selectCount(
table: String,
schema: String? = null,
columns: String = "*",
count: CountOption = CountOption.EXACT,
headers: Map<String, String> = emptyMap(),
block: QueryBuilder.() -> Unit = {},
): SupabaseResult<PostgrestRange>Issues a count-only HEAD request and returns the total parsed from the
Content-Range header. No rows are fetched.
count—EXACT(accurate, slowest),PLANNED, orESTIMATED(faster, approximate). DefaultCountOption.EXACT.- Other parameters as for
select.
Returns: SupabaseResult<PostgrestRange> — its count holds the total.
val total = database.selectCount("todos") { where { Todo.done eq true } }
// total.value.countselectRange (interface method)
public suspend fun selectRange(
table: String,
schema: String? = null,
columns: String = "*",
format: ResponseFormat = ResponseFormat.ROWS,
count: CountOption = CountOption.EXACT,
stripNulls: Boolean = false,
headers: Map<String, String> = emptyMap(),
block: QueryBuilder.() -> Unit = {},
): SupabaseResult<PostgrestRawPage>Like select, but also returns the total and the fetched range from
Content-Range alongside the raw body, packaged as a
PostgrestRawPage (a data class with body, count, and
range — not a Pair). The typed selectWithCountTyped sits
on top of this and decodes body into a PostgrestPage.
count— total computation strategy. DefaultCountOption.EXACT.- Other parameters as for
select.
Returns: SupabaseResult<PostgrestRawPage>.
val raw = database.selectRange("todos") { range(0, 9) }
// raw.value.body (String), raw.value.count (Long?), raw.value.range (LongRange?)paginator
public inline fun <reified T> DatabaseClient.paginator(
table: String,
pageSize: Int = 20,
schema: String? = null,
columns: String = "*",
crossinline block: QueryBuilder.() -> Unit = {},
): Paginator<T>Builds a demand-driven Paginator over table, fetching one offset-based page per
Paginator.loadNext() via selectTypedOrThrow and a range window. The paginator
exposes accumulated rows and loading/end/error state as StateFlows; nothing is
fetched until you call loadNext().
table— table or view name.pageSize— rows per page; must be greater than 0. Default20.schema— target schema. Defaultnull.columns—select=projection. Default"*".block— ordering and predicates; alwaysorderBya stable column. Do not add your ownrange/limit/offset— the paginator owns the window.
Returns: Paginator<T> (from supabase-core).
val pager = database.paginator<Todo>("todos", pageSize = 20) {
orderBy(Todo.createdAt, order = Order.DESC)
}
pager.loadNext()Writes
insert (interface method)
public suspend fun insert(
table: String,
schema: String? = null,
body: String,
columns: List<String>? = null,
upsert: Boolean = false,
upsertResolution: UpsertResolution = UpsertResolution.MERGE_DUPLICATES,
defaultToNull: Boolean = true,
onConflict: String? = null,
returning: ReturnOption = ReturnOption.REPRESENTATION,
count: CountOption? = null,
stripNulls: Boolean = false,
rollback: Boolean = false,
contentType: String = "application/json",
headers: Map<String, String> = emptyMap(),
): SupabaseResult<String>Inserts (or upserts) into table via POST /rest/v1/{table}, with body the JSON
of a single object or an array (bulk insert).
table— target table.schema— target schema. Defaultnull.body— the JSON object or array to write.columns— explicit column list (columns=); when null it is derived for bulk inserts as the union of all rows’ keys. Defaultnull.upsert— when true, turns this into an upsert (on_conflict=+resolution=). Defaultfalse.upsertResolution— conflict resolution when upserting (seeUpsertResolution). DefaultMERGE_DUPLICATES.defaultToNull— whenfalse, sendsmissing=defaultso omitted fields take their column DEFAULT instead of NULL. Defaulttrue.onConflict— the unique column(s) to match on for upsert. Defaultnull.returning— whether affected rows are echoed back or the body is empty (seeReturnOption). DefaultREPRESENTATION.count— adds acount=preference, surfacing the affected total inContent-Range. Defaultnull.stripNulls— omit null fields from the response. Defaultfalse.rollback— when true, sendstx=rollbackso the write is validated but not committed. Defaultfalse.contentType— the request body’sContent-Type; lets callers send e.g. atext/csvbulk insert. Only the header changes —bodyis sent verbatim. Default"application/json".headers— extra request headers. DefaultemptyMap().
Returns: SupabaseResult<String> — representation rows, or empty for MINIMAL.
database.insert("todos", body = """{"title":"Buy milk"}""")insertTyped
public suspend inline fun <reified T> DatabaseClient.insertTyped(
table: String,
schema: String? = null,
value: T,
columns: List<String>? = null,
upsert: Boolean = false,
upsertResolution: UpsertResolution = UpsertResolution.MERGE_DUPLICATES,
defaultToNull: Boolean = true,
onConflict: String? = null,
): SupabaseResult<List<T>>Inserts a single value (serialized to JSON) and returns the inserted row(s)
decoded as List<T> — insert with REPRESENTATION. DB defaults/triggers are
reflected in the decoded result.
value— the object to insert.- Other parameters as on
insert.
Returns: SupabaseResult<List<T>>.
val inserted = database.insertTyped("todos", value = Todo(title = "Buy milk"))insertTypedMany
public suspend inline fun <reified T> DatabaseClient.insertTypedMany(
table: String,
schema: String? = null,
values: List<T>,
columns: List<String>? = null,
upsert: Boolean = false,
upsertResolution: UpsertResolution = UpsertResolution.MERGE_DUPLICATES,
defaultToNull: Boolean = true,
onConflict: String? = null,
): SupabaseResult<List<T>>Bulk-inserts values in one request and returns the inserted rows. The union of
all elements’ keys is sent as columns=, so a field present on only some rows is
still inserted rather than dropped from first-row inference.
values— the objects to insert.- Other parameters as on
insert.
Returns: SupabaseResult<List<T>>.
database.insertTypedMany("todos", values = listOf(Todo("a"), Todo("b")))insertUnit
public suspend fun DatabaseClient.insertUnit(
table: String,
schema: String? = null,
body: String,
columns: List<String>? = null,
upsert: Boolean = false,
upsertResolution: UpsertResolution = UpsertResolution.MERGE_DUPLICATES,
defaultToNull: Boolean = true,
onConflict: String? = null,
count: CountOption? = null,
): SupabaseResult<Unit>Inserts the raw JSON body without fetching the result back (ReturnOption.MINIMAL),
saving the round-trip payload. body may be a single object or an array.
count— when set, learn how many rows were written viaContent-Range. Defaultnull.- Other parameters as on
insert.
Returns: SupabaseResult<Unit>.
database.insertUnit("todos", body = """{"title":"Buy milk"}""")insertUnitTyped
public suspend inline fun <reified T> DatabaseClient.insertUnitTyped(
table: String,
schema: String? = null,
value: T,
columns: List<String>? = null,
upsert: Boolean = false,
upsertResolution: UpsertResolution = UpsertResolution.MERGE_DUPLICATES,
defaultToNull: Boolean = true,
onConflict: String? = null,
count: CountOption? = null,
): SupabaseResult<Unit>The typed counterpart to insertUnit: serializes value to JSON and inserts it
without fetching it back.
value— the object to insert.- Other parameters as on
insertUnit.
Returns: SupabaseResult<Unit>.
database.insertUnitTyped("todos", value = Todo(title = "Buy milk"))upsertTyped
public suspend inline fun <reified T> DatabaseClient.upsertTyped(
table: String,
schema: String? = null,
value: T,
columns: List<String>? = null,
upsertResolution: UpsertResolution = UpsertResolution.MERGE_DUPLICATES,
defaultToNull: Boolean = true,
onConflict: String? = null,
): SupabaseResult<List<T>>Upserts a single value (insert-or-update on conflict) and returns the resulting
row(s) — insertTyped with upsert = true under a clearer name.
onConflict— the unique column(s) PostgREST matches on. Defaultnull.upsertResolution— merge or ignore existing rows. DefaultMERGE_DUPLICATES.- Other parameters as on
insertTyped.
Returns: SupabaseResult<List<T>>.
database.upsertTyped("todos", value = todo, onConflict = "id")upsertTypedMany
public suspend inline fun <reified T> DatabaseClient.upsertTypedMany(
table: String,
schema: String? = null,
values: List<T>,
columns: List<String>? = null,
upsertResolution: UpsertResolution = UpsertResolution.MERGE_DUPLICATES,
defaultToNull: Boolean = true,
onConflict: String? = null,
): SupabaseResult<List<T>>Bulk upserts values in one request and returns the resulting rows — the
multi-row form of upsertTyped.
- Parameters as on
upsertTyped, withvaluesin place ofvalue.
Returns: SupabaseResult<List<T>>.
database.upsertTypedMany("todos", values = todos, onConflict = "id")update (interface method)
public suspend fun update(
table: String,
schema: String? = null,
body: String,
returning: ReturnOption = ReturnOption.REPRESENTATION,
count: CountOption? = null,
stripNulls: Boolean = false,
rollback: Boolean = false,
maxAffected: Int? = null,
explain: ExplainOptions? = null,
headers: Map<String, String> = emptyMap(),
block: WhereBuilder.() -> Unit = {},
): SupabaseResult<String>Updates rows matching the filters via PATCH /rest/v1/{table}, applying the
partial JSON object in body to every matched row. The mutation block is a
filter-only WhereBuilder — there is no order/limit. An empty filter set
updates the whole table.
table— target table.schema— target schema. Defaultnull.body— the partial JSON object to apply.returning— shape the echoed body (seeReturnOption). DefaultREPRESENTATION.count— adds acount=preference, surfacing the affected total. Defaultnull.stripNulls— omit null fields from the response. Defaultfalse.rollback— validate without committing (tx=rollback). Defaultfalse.maxAffected— when set (must be > 0), caps the rows modified (handling=strict+max-affected=), failing rather than touching more. Defaultnull.explain— returns the query plan instead of mutating. Defaultnull.headers— extra request headers. DefaultemptyMap().block— the filter DSL (filter only).
Returns: SupabaseResult<String>.
database.update("todos", body = """{"done":true}""") { Todo.id eq 7 }updateTyped
public suspend inline fun <reified T> DatabaseClient.updateTyped(
table: String,
schema: String? = null,
value: T,
noinline block: WhereBuilder.() -> Unit = {},
): SupabaseResult<List<T>>Updates rows matching the filters with the fields of value and returns the
updated rows. value is applied as a partial update.
null fields of value are omitted from the request (the serializer uses
explicitNulls = false), so they leave the existing column untouched rather than
overwriting it with NULL. To clear a column to NULL, use update
with a raw JSON body (e.g. {"avatar_url": null}).
value— the object whose non-null fields are written.block— the filter DSL.
Returns: SupabaseResult<List<T>>.
database.updateTyped("todos", value = Todo(done = true)) { Todo.id eq 7 }updateUnit
public suspend fun DatabaseClient.updateUnit(
table: String,
schema: String? = null,
body: String,
count: CountOption? = null,
block: WhereBuilder.() -> Unit = {},
): SupabaseResult<Unit>Updates rows matching the filters with the raw JSON body without fetching the
result back (ReturnOption.MINIMAL). An empty filter block updates every row.
count— learn how many rows changed viaContent-Range. Defaultnull.- Other parameters as on
update.
Returns: SupabaseResult<Unit>.
database.updateUnit("todos", body = """{"done":true}""") { Todo.id eq 7 }updateUnitTyped
public suspend inline fun <reified T> DatabaseClient.updateUnitTyped(
table: String,
schema: String? = null,
value: T,
count: CountOption? = null,
noinline block: WhereBuilder.() -> Unit = {},
): SupabaseResult<Unit>The typed counterpart to updateUnit: serializes value and updates without
fetching rows back. As with updateTyped, null fields of value are omitted
(not written as NULL).
value— the object whose non-null fields are written.count— affected total viaContent-Range. Defaultnull.block— the filter DSL.
Returns: SupabaseResult<Unit>.
database.updateUnitTyped("todos", value = Todo(done = true)) { Todo.id eq 7 }replace (interface method)
public suspend fun replace(
table: String,
body: String,
returning: ReturnOption = ReturnOption.REPRESENTATION,
columns: String = "*",
block: WhereBuilder.() -> Unit = {},
): SupabaseResult<String>Replaces (or inserts) a single row via PUT /rest/v1/{table} — the
replace-by-primary-key write. Unlike update’s partial PATCH, this is a full-row
replace: body must carry every column (including the primary key), and the
filter must select exactly that primary key. PostgREST replaces the matching
row, or inserts the row when none matches.
table— target table.body— the complete JSON object (every column).returning— whether the affected row is echoed back or the body is empty. DefaultREPRESENTATION.columns— theselect=projection applied to the returned representation. Default"*".block— the filter DSL, selecting exactly the primary key.
Returns: SupabaseResult<String> — the replaced row, or empty for non-representation returns.
database.replace("todos", body = """{"id":7,"title":"Buy milk","done":true}""") {
Todo.id eq 7
}delete (interface method)
public suspend fun delete(
table: String,
schema: String? = null,
returning: ReturnOption = ReturnOption.REPRESENTATION,
count: CountOption? = null,
stripNulls: Boolean = false,
rollback: Boolean = false,
maxAffected: Int? = null,
explain: ExplainOptions? = null,
headers: Map<String, String> = emptyMap(),
block: WhereBuilder.() -> Unit = {},
): SupabaseResult<String>Deletes rows matching the filters via DELETE /rest/v1/{table}. As with update,
the rows removed are exactly those the filter selects, so an empty filter set
deletes the whole table — guard large or accidental deletes with maxAffected.
table— target table.schema— target schema. Defaultnull.returning— whether deleted rows are echoed back or the body is empty. DefaultREPRESENTATION.count— affected total viaContent-Range. Defaultnull.stripNulls— omit null fields from the response. Defaultfalse.rollback— validate without committing. Defaultfalse.maxAffected— when set (> 0), caps the rows deleted, failing rather than removing more. Defaultnull.explain— returns the query plan instead of deleting. Defaultnull.headers— extra request headers. DefaultemptyMap().block— the filter DSL.
Returns: SupabaseResult<String>.
database.delete("todos") { Todo.id eq 7 }deleteTyped
public suspend inline fun <reified T> DatabaseClient.deleteTyped(
table: String,
schema: String? = null,
returning: ReturnOption = ReturnOption.REPRESENTATION,
count: CountOption? = null,
noinline block: WhereBuilder.() -> Unit = {},
): SupabaseResult<List<T>>Deletes rows matching the filters and returns the deleted rows as List<T> —
useful when you need the removed data (e.g. to undo or audit).
- Parameters as on
delete.
Returns: SupabaseResult<List<T>>.
val removed = database.deleteTyped<Todo>("todos") { Todo.done eq true }deleteUnit
public suspend fun DatabaseClient.deleteUnit(
table: String,
schema: String? = null,
count: CountOption? = null,
block: WhereBuilder.() -> Unit = {},
): SupabaseResult<Unit>Deletes rows matching the filters without fetching them back
(ReturnOption.MINIMAL). An empty filter block deletes every row.
count— learn how many rows were removed viaContent-Range. Defaultnull.- Other parameters as on
delete.
Returns: SupabaseResult<Unit>.
database.deleteUnit("todos") { Todo.id eq 7 }RPC (stored procedures)
PostgREST exposes stored procedures both as POST /rest/v1/rpc/{function} (the
mutating form, in rpc) and GET /rest/v1/rpc/{function}
(the cacheable, read-only form, in rpcGet). Each has a
family of typed wrappers; most come in two overloads — one taking a raw JSON
params string (or a Map for GET), and one taking a serializable params
object that is encoded for you.
rpc (interface method)
public suspend fun rpc(
function: String,
schema: String? = null,
params: String? = null,
format: ResponseFormat = ResponseFormat.ROWS,
count: CountOption? = null,
stripNulls: Boolean = false,
rollback: Boolean = false,
maxAffected: Int? = null,
explain: ExplainOptions? = null,
contentType: String = "application/json",
headers: Map<String, String> = emptyMap(),
block: QueryBuilder.() -> Unit = {},
): SupabaseResult<String>Calls a stored procedure (POST) with params as the JSON request body. Suitable
for functions with side effects or large argument payloads; for a read-only
function prefer rpcGet.
function— the function name.schema— target schema. Defaultnull.params— JSON object of named arguments, or null for a no-argument call. Defaultnull.format— body shape (seeResponseFormat). DefaultROWS.count— adds acount=preference. Defaultnull.stripNulls— omit null fields from the response. Defaultfalse.rollback— roll back any writes (tx=rollback). Defaultfalse.maxAffected— when set (> 0), caps rows the function may modify. Defaultnull.explain— returns the plan. Defaultnull.contentType— request bodyContent-Type; lets you send a scalar body (e.g.text/plain) to a single-parameter function.paramsis sent verbatim. Default"application/json".headers— extra request headers. DefaultemptyMap().block— PostgREST filters/ordering/pagination applied to a set-returning function’s rows (the query DSL).
Returns: SupabaseResult<String>.
database.rpc("increment", params = """{"row_id":7}""")rpcTyped
public suspend inline fun <reified T> DatabaseClient.rpcTyped(
function: String, schema: String? = null, params: String? = null,
): SupabaseResult<T>
public suspend inline fun <reified Request : Any, reified Response> DatabaseClient.rpcTyped(
function: String, schema: String? = null, params: Request,
): SupabaseResult<Response>Calls a stored procedure (POST) and decodes its result into T/Response. The
second overload serializes a params object to a JSON object for you.
function— the function name.schema— target schema. Defaultnull.params— raw JSON string (or null), or a serializable arguments object.
Returns: SupabaseResult<T> (or SupabaseResult<Response>).
val total: SupabaseResult<Int> = database.rpcTyped("count_open_todos")rpcListTyped
public suspend inline fun <reified T> DatabaseClient.rpcListTyped(
function: String, schema: String? = null, params: String? = null,
): SupabaseResult<List<T>>
public suspend inline fun <reified Request : Any, reified Response> DatabaseClient.rpcListTyped(
function: String, schema: String? = null, params: Request,
): SupabaseResult<List<Response>>The list-result form of rpcTyped for set-returning functions: decodes into
List<T>/List<Response>.
- Parameters as on
rpcTyped.
Returns: SupabaseResult<List<T>>.
val rows = database.rpcListTyped<Todo>("search_todos", params = """{"q":"milk"}""")rpcSingleTyped
public suspend inline fun <reified T> DatabaseClient.rpcSingleTyped(
function: String, schema: String? = null, params: String? = null,
): SupabaseResult<T>
public suspend inline fun <reified Request : Any, reified Response> DatabaseClient.rpcSingleTyped(
function: String, schema: String? = null, params: Request,
): SupabaseResult<Response>Calls a procedure expecting exactly one row/scalar, decoded into T. Requests
format = SINGLE, so zero or more than one row fails with HTTP 406 / NOT_FOUND.
- Parameters as on
rpcTyped.
Returns: SupabaseResult<T>.
val one = database.rpcSingleTyped<Todo>("latest_todo")rpcMaybeSingleTyped
public suspend inline fun <reified T> DatabaseClient.rpcMaybeSingleTyped(
function: String, schema: String? = null, params: String? = null,
): SupabaseResult<T?>
public suspend inline fun <reified Request : Any, reified Response> DatabaseClient.rpcMaybeSingleTyped(
function: String, schema: String? = null, params: Request,
): SupabaseResult<Response?>The lenient variant of rpcSingleTyped: a “no rows” response (HTTP 406 /
NOT_FOUND) maps to Success(null); other failures propagate.
- Parameters as on
rpcTyped.
Returns: SupabaseResult<T?>.
val maybe = database.rpcMaybeSingleTyped<Todo>("latest_todo")rpcUnit
public suspend fun DatabaseClient.rpcUnit(
function: String, schema: String? = null, params: String? = null,
): SupabaseResult<Unit>
public suspend inline fun <reified Request : Any> DatabaseClient.rpcUnit(
function: String, schema: String? = null, params: Request,
): SupabaseResult<Unit>Calls a procedure (POST) for its side effects, discarding the result.
- Parameters as on
rpcTyped.
Returns: SupabaseResult<Unit>.
database.rpcUnit("reset_counters")rpcCsv
public suspend fun DatabaseClient.rpcCsv(
function: String, schema: String? = null, params: String? = null,
): SupabaseResult<String>
public suspend inline fun <reified Request : Any> DatabaseClient.rpcCsv(
function: String, schema: String? = null, params: Request,
): SupabaseResult<String>Calls a procedure (POST) requesting text/csv, returning the raw CSV string.
- Parameters as on
rpcTyped.
Returns: SupabaseResult<String>.
val csv = database.rpcCsv("export_todos")rpcHead
public suspend fun DatabaseClient.rpcHead(
function: String, schema: String? = null, params: String? = null,
count: CountOption? = null,
): SupabaseResult<Unit>
public suspend inline fun <reified Request : Any> DatabaseClient.rpcHead(
function: String, schema: String? = null, params: Request,
count: CountOption? = null,
): SupabaseResult<Unit>Issues a procedure (POST) as a HEAD request for its headers/count only,
discarding the empty body.
count— when set, the total is reported viaContent-Range. Defaultnull.- Other parameters as on
rpcTyped.
Returns: SupabaseResult<Unit>.
database.rpcHead("count_open_todos", count = CountOption.EXACT)rpcGet (interface method)
public suspend fun rpcGet(
function: String,
schema: String? = null,
queryParams: List<Pair<String, String>> = emptyList(),
format: ResponseFormat = ResponseFormat.ROWS,
count: CountOption? = null,
stripNulls: Boolean = false,
explain: ExplainOptions? = null,
retry: Boolean = true,
headers: Map<String, String> = emptyMap(),
): SupabaseResult<String>Calls a stored procedure over GET, passing arguments as queryParams rather than
a body — the cacheable, side-effect-free counterpart to rpc for functions marked
STABLE/IMMUTABLE.
function— the function name.schema— target schema. Defaultnull.queryParams— ordered list of name/value argument pairs. DefaultemptyList().format— body shape. DefaultROWS.count— adds acount=preference. Defaultnull.stripNulls— omit null fields. Defaultfalse.explain— returns the plan. Defaultnull.retry— whether this read may be transparently retried. Defaulttrue.headers— extra request headers. DefaultemptyMap().
Returns: SupabaseResult<String>.
database.rpcGet("hello", queryParams = listOf("name" to "Ada"))rpcGet (Map and object overloads)
public suspend fun DatabaseClient.rpcGet(
function: String, schema: String? = null,
queryParams: Map<String, String>,
): SupabaseResult<String>
public suspend fun DatabaseClient.rpcGet(
function: String, schema: String? = null,
queryParams: Map<String, String>,
format: ResponseFormat = ResponseFormat.ROWS,
count: CountOption? = null,
): SupabaseResult<String>
public suspend inline fun <reified Request : Any> DatabaseClient.rpcGet(
function: String, schema: String? = null, params: Request,
): SupabaseResult<String>Map-based and serializable-object conveniences over the pair-list interface
method. The object overload flattens the request’s top-level fields into query
arguments (object/array values re-encoded as JSON strings); a null argument is
omitted so the function receives its SQL default.
queryParams— named arguments as aMap(RPC arguments are named).params— a serializable arguments object.format/count— as onrpcGet.
Returns: SupabaseResult<String>.
database.rpcGet("hello", queryParams = mapOf("name" to "Ada"))rpcGetTyped
public suspend inline fun <reified T> DatabaseClient.rpcGetTyped(
function: String, schema: String? = null,
queryParams: Map<String, String> = emptyMap(),
): SupabaseResult<T>
public suspend inline fun <reified Request : Any, reified Response> DatabaseClient.rpcGetTyped(
function: String, schema: String? = null, params: Request,
): SupabaseResult<Response>The GET counterpart of rpcTyped: calls a read-only procedure and decodes the
result into T/Response.
queryParams— named arguments. DefaultemptyMap().params— a serializable arguments object.
Returns: SupabaseResult<T>.
val greeting = database.rpcGetTyped<String>("hello", mapOf("name" to "Ada"))rpcGetListTyped
public suspend inline fun <reified T> DatabaseClient.rpcGetListTyped(
function: String, schema: String? = null,
queryParams: Map<String, String> = emptyMap(),
): SupabaseResult<List<T>>
public suspend inline fun <reified Request : Any, reified Response> DatabaseClient.rpcGetListTyped(
function: String, schema: String? = null, params: Request,
): SupabaseResult<List<Response>>The GET counterpart of rpcListTyped for set-returning read-only functions.
- Parameters as on
rpcGetTyped.
Returns: SupabaseResult<List<T>>.
val rows = database.rpcGetListTyped<Todo>("search_todos", mapOf("q" to "milk"))rpcGetSingleTyped
public suspend inline fun <reified T> DatabaseClient.rpcGetSingleTyped(
function: String, schema: String? = null,
queryParams: Map<String, String> = emptyMap(),
): SupabaseResult<T>
public suspend inline fun <reified Request : Any, reified Response> DatabaseClient.rpcGetSingleTyped(
function: String, schema: String? = null, params: Request,
): SupabaseResult<Response>The GET counterpart of rpcSingleTyped. Fails with HTTP 406 / NOT_FOUND when not
exactly one row is returned.
- Parameters as on
rpcGetTyped.
Returns: SupabaseResult<T>.
val one = database.rpcGetSingleTyped<Todo>("latest_todo")rpcGetMaybeSingleTyped
public suspend inline fun <reified T> DatabaseClient.rpcGetMaybeSingleTyped(
function: String, schema: String? = null,
queryParams: Map<String, String> = emptyMap(),
): SupabaseResult<T?>
public suspend inline fun <reified Request : Any, reified Response> DatabaseClient.rpcGetMaybeSingleTyped(
function: String, schema: String? = null, params: Request,
): SupabaseResult<Response?>The GET, lenient counterpart of rpcSingleTyped: “no rows” (HTTP 406 / NOT_FOUND)
maps to Success(null).
- Parameters as on
rpcGetTyped.
Returns: SupabaseResult<T?>.
val maybe = database.rpcGetMaybeSingleTyped<Todo>("latest_todo")rpcGetUnit
public suspend fun DatabaseClient.rpcGetUnit(
function: String, schema: String? = null,
queryParams: Map<String, String> = emptyMap(),
): SupabaseResult<Unit>
public suspend inline fun <reified Request : Any> DatabaseClient.rpcGetUnit(
function: String, schema: String? = null, params: Request,
): SupabaseResult<Unit>Calls a read-only procedure (GET) for its effect alone, discarding the body.
- Parameters as on
rpcGetTyped.
Returns: SupabaseResult<Unit>.
database.rpcGetUnit("ping")rpcGetCsv
public suspend fun DatabaseClient.rpcGetCsv(
function: String, schema: String? = null,
queryParams: Map<String, String> = emptyMap(),
): SupabaseResult<String>
public suspend inline fun <reified Request : Any> DatabaseClient.rpcGetCsv(
function: String, schema: String? = null, params: Request,
): SupabaseResult<String>The GET counterpart of rpcCsv: requests text/csv and returns the raw CSV string.
- Parameters as on
rpcGetTyped.
Returns: SupabaseResult<String>.
val csv = database.rpcGetCsv("export_todos")rpcGetHead
public suspend fun DatabaseClient.rpcGetHead(
function: String, schema: String? = null,
queryParams: Map<String, String> = emptyMap(),
count: CountOption? = null,
): SupabaseResult<Unit>
public suspend inline fun <reified Request : Any> DatabaseClient.rpcGetHead(
function: String, schema: String? = null, params: Request,
count: CountOption? = null,
): SupabaseResult<Unit>The GET counterpart of rpcHead: issues a HEAD for headers/count only,
discarding the empty body.
count— when set, total viaContent-Range. Defaultnull.- Other parameters as on
rpcGetTyped.
Returns: SupabaseResult<Unit>.
database.rpcGetHead("count_open_todos", count = CountOption.EXACT)Query and filter DSL
Reads take a QueryBuilder block (filters plus result modifiers); mutations
(update/delete/replace) take a filter-only WhereBuilder block. These DSL
types are provided by supabase-core (io.github.androidpoet.supabase.core.models)
and re-used here.
QueryBuilder
The receiver of the read-query block. Combines a where { } predicate with result
modifiers; modifiers are kept distinct from filters.
| Member | Signature | Effect |
|---|---|---|
where | where(block: WhereBuilder.() -> Unit) | Filter predicate; multiple statements are AND-ed. |
orderBy | orderBy(column: Column<*>, order: Order = Order.ASC, nulls: Nulls? = null, referencedTable: String? = null) | Sort key; successive calls add secondary keys into one order= param. |
limit | limit(count: Int, referencedTable: String? = null) | Cap rows; last call wins. |
offset | offset(count: Int, referencedTable: String? = null) | Skip rows; last call wins. |
range | range(from: Int, to: Int, referencedTable: String? = null) | Inclusive zero-based window [from, to], emitted as offset + derived limit. Requires to >= from. |
database.selectTyped<Todo>("todos") {
where { Todo.done eq false }
orderBy(Todo.createdAt, order = Order.DESC, nulls = Nulls.LAST)
range(0, 19)
}WhereBuilder
Builds a Filter from column predicates combined with infix operators. Each
statement adds one predicate; multiple statements are AND-ed. String operands are
escaped per PostgREST’s quoting rules; numbers and booleans are emitted verbatim.
Comparison: eq, neq, greater, greaterEq, less, lessEq,
within (a ClosedRange, inclusive), isDistinctFrom (null-aware IS DISTINCT FROM).
Null / boolean IS: isNull(), isNotNull(), isExactly (IS TRUE/FALSE).
Pattern matching: like, ilike, matches (POSIX ~), imatches (~*),
likeAllOf, likeAnyOf, ilikeAllOf, ilikeAnyOf (each taking a List<String>).
Membership: inList, notInList (each taking a List<T>).
Array: contains (@>), containedBy (<@), overlaps (&&) — on Column<List<T>>.
Range columns: rangeGt (sr), rangeGte (nxl), rangeLt (sl),
rangeLte (nxr), rangeAdjacent (adj) — each takes a range literal String.
Full-text search: textSearch(query: String, config: String? = null, type: TextSearchType = TextSearchType.PLAIN).
Escape hatch: raw(column: Column<*>, operator: String, value: String) — emits column=<operator>.<value> for operators the DSL doesn’t expose yet.
Logical groups: or(referencedTable: String? = null, block), and(referencedTable: String? = null, block), not(block).
database.deleteTyped<Todo>("todos") {
Todo.done eq true
or {
Todo.priority greater 5
Todo.title ilike "%urgent%"
}
}Column
@JvmInline
public value class Column<T>(public val name: String)A type-safe handle to a column, carrying its wire name and Kotlin type T so the
compiler rejects type-mismatched comparisons. Declare columns by hand, or let
codegen emit a typed schema object.
object Todo {
val id = Column<Int>("id")
val title = Column<String>("title")
val done = Column<Boolean>("done")
}Models and enums
ResponseFormat
public enum class ResponseFormat { ROWS, SINGLE, CSV, GEOJSON, HEAD }The mutually-exclusive body shape a read requests via its Accept header (one enum
rather than parallel single/csv/geojson/head booleans, so illegal
combinations are unrepresentable).
ROWS— the default JSON array.SINGLE— expects exactly one row (application/vnd.pgrst.object+json; 406 otherwise).CSV— returnstext/csv.GEOJSON— returns a PostGISFeatureCollection.HEAD— requests headers only and yields an empty body; pair with acountto fetch just a total.
database.select("stores", format = ResponseFormat.GEOJSON)CountOption
public enum class CountOption { EXACT, PLANNED, ESTIMATED }How PostgREST should compute the total row count (the count= directive of the
Prefer header); the result is surfaced in Content-Range.
EXACT— full count, accurate but slowest.PLANNED— the query planner’s estimate (fast).ESTIMATED— the planner estimate, possibly falling back to an exact count for small results.
ReturnOption
public enum class ReturnOption { MINIMAL, REPRESENTATION, HEADERS_ONLY }What a write should return (the return= directive of the Prefer header).
MINIMAL— empty body (used by the*Unithelpers).REPRESENTATION— echoes the affected rows back (so typed helpers can decode them).HEADERS_ONLY— empty body but keeps headers such asLocationpopulated.
UpsertResolution
public enum class UpsertResolution { MERGE_DUPLICATES, IGNORE_DUPLICATES }How an upsert resolves a conflict on the target key (the resolution= directive),
used when upsert is enabled.
MERGE_DUPLICATES— update the existing row with the incoming values.IGNORE_DUPLICATES— leave the existing row untouched.
ExplainFormat
public enum class ExplainFormat { TEXT, JSON, XML, YAML }Output format for an EXPLAIN plan, selecting the
application/vnd.pgrst.plan+<format> media type via ExplainOptions.
ExplainOptions
public data class ExplainOptions(
public val analyze: Boolean = false,
public val verbose: Boolean = false,
public val settings: Boolean = false,
public val buffers: Boolean = false,
public val wal: Boolean = false,
public val format: ExplainFormat = ExplainFormat.TEXT,
)Requests a PostgREST query plan (EXPLAIN) instead of executing the request for its
result. Pass an instance to select, update, delete, or the rpc calls to have
them return the plan text in the body rather than rows.
analyze— actually run the query and report real timings, not just the estimate. Defaultfalse.verbose— include additional per-node detail. Defaultfalse.settings— include configuration parameters that affect planning. Defaultfalse.buffers— include buffer usage (requiresanalyze). Defaultfalse.wal— include write-ahead-log usage (requiresanalyze). Defaultfalse.format— the rendering format (seeExplainFormat). DefaultExplainFormat.TEXT.
val plan = database.select("todos", explain = ExplainOptions(analyze = true))PostgrestRange
public data class PostgrestRange(
public val count: Long? = null,
public val range: LongRange? = null,
)The count and row range parsed from a Content-Range response header. For
Content-Range: 0-9/27, range is 0..9 and count is 27. Either part may be
absent — * maps to null rather than failing. Returned by
selectCount.
PostgrestRawPage
public data class PostgrestRawPage(
public val body: String,
public val count: Long? = null,
public val range: LongRange? = null,
)The raw response body of a selectRange together
with the total count and fetched range from Content-Range. The string analogue
of PostgrestPage — not a Pair. The typed selectWithCountTyped helper decodes
body into a PostgrestPage.
PostgrestPage
public data class PostgrestPage<T>(
public val rows: List<T>,
public val count: Long? = null,
public val range: LongRange? = null,
)A page of decoded rows together with the total count reported by PostgREST.
Produced by selectWithCountTyped.
Design notes
select returns String; selectTyped is a separate name
The interface method select(...) returns SupabaseResult<String> — the raw JSON
escape hatch — while decoding into a List<T> is done by the separate reified inline extension selectTyped<T> (and selectSingleTyped<T> → T,
selectMaybeSingleTyped<T> → T?). This split is deliberate and forced by the
language:
- Decoding the body into
Tneeds the concrete type at runtime, which on Kotlin Multiplatform means areifiedtype parameter. - A
reifiedtype parameter requires aninlinefunction, and an interface method cannot beinline(there is no body to inline at the call site, and a virtual call can’t carry a reified type). So the typed variant cannot live on theDatabaseClientinterface — it has to be a top-level extension. - A single function can’t return both
StringandList<T>, and adding a typed overload ofselectwould create resolution ambiguity (the compiler couldn’t always tell the raw call from the typed one). Giving the typed variant its own name —selectTyped— removes the ambiguity and makes the choice explicit at the call site.
The same reasoning produces the *Typed/*Unit/*Csv naming across the whole
module: the interface holds the small set of raw, string-returning primitives
(select, insert, update, replace, delete, rpc, rpcGet,
selectCount, selectRange), and the ergonomic typed layer is a set of reified
extensions on top.
Raw primitives vs. the decoding layer
Keeping the wire-level methods on the interface and the serialization in extensions
separates two concerns cleanly. The interface is a thin, mockable contract that owns
exactly one thing — turning a call into one HTTP request and handing back the body
and headers. Serialization policy (which kotlinx.serialization settings, how
null fields are treated, how a “no rows” 406 becomes Success(null)) lives
entirely in the extension layer, where it can evolve without changing the interface
and where every helper is a pure, testable transformation over the same primitive.
It also leaves the raw String methods available as an escape hatch for responses
the typed helpers don’t model (custom media types, hand-built queries, streaming the
body elsewhere).