postbase 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- postbase-0.1.0/PKG-INFO +755 -0
- postbase-0.1.0/README.md +741 -0
- postbase-0.1.0/pyproject.toml +67 -0
- postbase-0.1.0/pyproject.toml.orig +48 -0
- postbase-0.1.0/src/postbase/__init__.py +89 -0
- postbase-0.1.0/src/postbase/_internal.py +158 -0
- postbase-0.1.0/src/postbase/_query_state.py +100 -0
- postbase-0.1.0/src/postbase/aio/__init__.py +25 -0
- postbase-0.1.0/src/postbase/aio/auth.py +584 -0
- postbase-0.1.0/src/postbase/aio/client.py +164 -0
- postbase-0.1.0/src/postbase/aio/email.py +54 -0
- postbase-0.1.0/src/postbase/aio/query.py +269 -0
- postbase-0.1.0/src/postbase/aio/storage.py +293 -0
- postbase-0.1.0/src/postbase/auth.py +592 -0
- postbase-0.1.0/src/postbase/client.py +171 -0
- postbase-0.1.0/src/postbase/email.py +54 -0
- postbase-0.1.0/src/postbase/py.typed +0 -0
- postbase-0.1.0/src/postbase/query.py +268 -0
- postbase-0.1.0/src/postbase/storage.py +291 -0
- postbase-0.1.0/src/postbase/types.py +233 -0
postbase-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: postbase
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The official Python client for Postbase — self-hosted backend as a service
|
|
5
|
+
Keywords: postbase,database,auth,storage,backend,self-hosted,postgresql,baas
|
|
6
|
+
Author: Postbase
|
|
7
|
+
Author-email: Postbase <harshalone@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Requires-Dist: httpx>=0.27.0
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Project-URL: Homepage, https://www.getpostbase.com
|
|
12
|
+
Project-URL: Repository, https://github.com/harshalone/postbasepy
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# postbasepy
|
|
16
|
+
|
|
17
|
+
The official Python client for [Postbase](https://www.getpostbase.com) — a self-hosted, open-source backend as a service.
|
|
18
|
+
|
|
19
|
+
[](https://pypi.org/project/postbase/)
|
|
20
|
+
[](https://github.com/harshalone/postbasepy/blob/main/LICENSE)
|
|
21
|
+
|
|
22
|
+
> **[getpostbase.com](https://www.getpostbase.com)** · [Documentation](https://www.getpostbase.com/docs) · [GitHub](https://github.com/harshalone/postbasepy)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<a href="https://www.youtube.com/watch?v=St_kJZXZ_nE">
|
|
28
|
+
<img src="https://img.youtube.com/vi/St_kJZXZ_nE/maxresdefault.jpg" alt="Postbase overview video" width="100%" />
|
|
29
|
+
</a>
|
|
30
|
+
<br/><em>▶ Watch: Postbase overview</em>
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## What is Postbase?
|
|
36
|
+
|
|
37
|
+
Postbase is a self-hosted backend platform built on PostgreSQL. It gives you a database with a REST query API, authentication (password, magic link, OTP, OAuth), file storage, and row-level security — all running on your own infrastructure.
|
|
38
|
+
|
|
39
|
+
`postbase` is the Python client SDK for interacting with your Postbase instance — sync and async, with the same chainable query builder as [`postbasejs`](https://www.npmjs.com/package/postbasejs).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Screenshots
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<img src="https://raw.githubusercontent.com/harshalone/postbase/main/images/1.png" alt="Postbase landing" width="100%" />
|
|
47
|
+
<br/><em>Self-hosted auth + database platform for Next.js</em>
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
<p align="center">
|
|
51
|
+
<img src="https://raw.githubusercontent.com/harshalone/postbase/main/images/2.png" alt="Dashboard" width="100%" />
|
|
52
|
+
<br/><em>Dashboard — manage organisations and projects</em>
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<p align="center">
|
|
56
|
+
<img src="https://raw.githubusercontent.com/harshalone/postbase/main/images/3.png" alt="Project overview" width="100%" />
|
|
57
|
+
<br/><em>Project overview with quick-start guide</em>
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
<p align="center">
|
|
61
|
+
<img src="https://raw.githubusercontent.com/harshalone/postbase/main/images/4.png" alt="Auth providers" width="100%" />
|
|
62
|
+
<br/><em>25+ auth providers — toggle any from the dashboard</em>
|
|
63
|
+
</p>
|
|
64
|
+
|
|
65
|
+
<p align="center">
|
|
66
|
+
<img src="https://raw.githubusercontent.com/harshalone/postbase/main/images/5.png" alt="SQL editor" width="100%" />
|
|
67
|
+
<br/><em>Built-in SQL editor with AI query generation</em>
|
|
68
|
+
</p>
|
|
69
|
+
|
|
70
|
+
<p align="center">
|
|
71
|
+
<img src="https://raw.githubusercontent.com/harshalone/postbase/main/images/6.png" alt="Storage connections" width="100%" />
|
|
72
|
+
<br/><em>S3-compatible storage — connect Amazon S3, Cloudflare R2, Backblaze B2, and more</em>
|
|
73
|
+
</p>
|
|
74
|
+
|
|
75
|
+
<p align="center">
|
|
76
|
+
<img src="https://raw.githubusercontent.com/harshalone/postbase/main/images/7.png" alt="Cron jobs" width="100%" />
|
|
77
|
+
<br/><em>Scheduled cron jobs — run SQL snippets or HTTP requests on any schedule</em>
|
|
78
|
+
</p>
|
|
79
|
+
|
|
80
|
+
<p align="center">
|
|
81
|
+
<img src="https://raw.githubusercontent.com/harshalone/postbase/main/images/8.png" alt="API keys" width="100%" />
|
|
82
|
+
<br/><em>API keys — anon and service role keys with SDK snippet</em>
|
|
83
|
+
</p>
|
|
84
|
+
|
|
85
|
+
<p align="center">
|
|
86
|
+
<img src="https://raw.githubusercontent.com/harshalone/postbase/main/images/9.png" alt="Project settings" width="100%" />
|
|
87
|
+
<br/><em>Project settings — configure auth redirect URLs, JWT expiry, and more</em>
|
|
88
|
+
</p>
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Installation
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install postbase
|
|
96
|
+
# or
|
|
97
|
+
uv add postbase
|
|
98
|
+
# or
|
|
99
|
+
poetry add postbase
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Requires Python 3.9+. Built on [`httpx`](https://www.python-httpx.org/), so both sync and async clients share one dependency.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Quick Start
|
|
107
|
+
|
|
108
|
+
**Sync:**
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from postbase import create_client
|
|
112
|
+
|
|
113
|
+
postbase = create_client(
|
|
114
|
+
"https://your-postbase-instance.com",
|
|
115
|
+
"pb_anon_your_api_key",
|
|
116
|
+
project_id="your-project-id",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
result = postbase.from_("posts").select().execute()
|
|
120
|
+
print(result.data, result.error)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Async:**
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from postbase.aio import create_async_client
|
|
127
|
+
|
|
128
|
+
postbase = create_async_client(
|
|
129
|
+
"https://your-postbase-instance.com",
|
|
130
|
+
"pb_anon_your_api_key",
|
|
131
|
+
project_id="your-project-id",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
result = await postbase.from_("posts").select().execute()
|
|
135
|
+
print(result.data, result.error)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Your **URL**, **anon key**, and **project ID** can be found in the API Keys section of your Postbase dashboard.
|
|
139
|
+
|
|
140
|
+
Every module in `postbase.aio` mirrors its sync counterpart in `postbase` method-for-method — only the client construction (`create_async_client`) and the `await` on each call differ. The rest of this README shows sync examples; add `await` and import from `postbase.aio` to use the async client.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Database
|
|
145
|
+
|
|
146
|
+
Query your PostgreSQL tables with a fluent, chainable API. Query builders are lazy — nothing is sent until you call `.execute()`, `.single()`, or `.maybe_single()` (or `await` an async builder directly, which does an implicit `.execute()`).
|
|
147
|
+
|
|
148
|
+
### Select
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
# Fetch all posts (wildcard or omit argument — both work)
|
|
152
|
+
result = postbase.from_("posts").select("*").execute()
|
|
153
|
+
result = postbase.from_("posts").select().execute()
|
|
154
|
+
|
|
155
|
+
# Select specific columns
|
|
156
|
+
result = postbase.from_("posts").select("id, title, created_at").execute()
|
|
157
|
+
|
|
158
|
+
# With filters
|
|
159
|
+
result = (
|
|
160
|
+
postbase.from_("posts")
|
|
161
|
+
.select("*")
|
|
162
|
+
.eq("status", "published")
|
|
163
|
+
.order("created_at", ascending=False)
|
|
164
|
+
.limit(10)
|
|
165
|
+
.execute()
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Get total count
|
|
169
|
+
result = postbase.from_("posts").select("*", count="exact").execute()
|
|
170
|
+
print(result.count)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Filter methods
|
|
174
|
+
|
|
175
|
+
Available on `select()`, `update()`, and `delete()` chains.
|
|
176
|
+
|
|
177
|
+
| Method | SQL equivalent |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `.eq(col, val)` | `col = val` |
|
|
180
|
+
| `.neq(col, val)` | `col != val` |
|
|
181
|
+
| `.gt(col, val)` | `col > val` |
|
|
182
|
+
| `.gte(col, val)` | `col >= val` |
|
|
183
|
+
| `.lt(col, val)` | `col < val` |
|
|
184
|
+
| `.lte(col, val)` | `col <= val` |
|
|
185
|
+
| `.like(col, pattern)` | `col LIKE pattern` |
|
|
186
|
+
| `.ilike(col, pattern)` | `col ILIKE pattern` |
|
|
187
|
+
| `.in_(col, values)` | `col IN (values)` |
|
|
188
|
+
| `.is_(col, None \| bool)` | `col IS NULL / TRUE / FALSE` |
|
|
189
|
+
| `.contains(col, val)` | `col @> val` |
|
|
190
|
+
| `.overlaps(col, val)` | `col && val` |
|
|
191
|
+
| `.text_search(col, query)` | full-text search |
|
|
192
|
+
| `.or_(filters)` | `col = val OR col = val` |
|
|
193
|
+
| `.not_(col, op, val)` | `NOT col op val` |
|
|
194
|
+
|
|
195
|
+
> `in_`, `is_`, `or_`, and `not_` have a trailing underscore — `in`, `is`, `or`, and `not` are Python keywords.
|
|
196
|
+
|
|
197
|
+
### `.or_()` — Supabase-compatible filter string
|
|
198
|
+
|
|
199
|
+
Pass a Supabase-style filter string and the SDK parses it into structured filters before sending to the server. Commas separate OR conditions; values with commas are safe inside parentheses (used by `in`).
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
# Simple OR: match either condition
|
|
203
|
+
result = postbase.from_("users").select().or_("email.ilike.%alice%,name.ilike.%alice%").execute()
|
|
204
|
+
|
|
205
|
+
# OR with in operator — values in parens are safe
|
|
206
|
+
result = postbase.from_("orders").select().or_("status.eq.active,status.in.(pending,review)").execute()
|
|
207
|
+
|
|
208
|
+
# Combine OR with AND filters — the .eq() is ANDed with the OR group
|
|
209
|
+
result = (
|
|
210
|
+
postbase.from_("posts")
|
|
211
|
+
.select()
|
|
212
|
+
.eq("published", True)
|
|
213
|
+
.or_("title.ilike.%hello%,body.ilike.%hello%")
|
|
214
|
+
.execute()
|
|
215
|
+
)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Supported operators inside `.or_()`:** `eq` `neq` `gt` `gte` `lt` `lte` `like` `ilike` `in` `is`
|
|
219
|
+
|
|
220
|
+
### Joins
|
|
221
|
+
|
|
222
|
+
Use `.join()` to combine data from related tables. Builders are immutable and can be stacked.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
# Left join — include orders even if no matching user
|
|
226
|
+
result = (
|
|
227
|
+
postbase.from_("orders")
|
|
228
|
+
.join("users", on="orders.user_id = users.id", type="left")
|
|
229
|
+
.select("orders.id, orders.total, users.email")
|
|
230
|
+
.execute()
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Multiple joins
|
|
234
|
+
result = (
|
|
235
|
+
postbase.from_("orders")
|
|
236
|
+
.join("users", on="orders.user_id = users.id", type="left")
|
|
237
|
+
.join("products", on="orders.product_id = products.id")
|
|
238
|
+
.select("orders.id, users.email, products.name")
|
|
239
|
+
.eq("orders.status", "active")
|
|
240
|
+
.order("orders.created_at", ascending=False)
|
|
241
|
+
.limit(20)
|
|
242
|
+
.execute()
|
|
243
|
+
)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Join types** (`type` defaults to `"inner"` if omitted): `"inner"`, `"left"`, `"right"`, `"full"`.
|
|
247
|
+
|
|
248
|
+
**`on` expression rules** — the server validates the `on` string against a strict allow-list:
|
|
249
|
+
|
|
250
|
+
- `table.column = table.column`
|
|
251
|
+
- Comparison operators: `=`, `<`, `>`, `!=`, `<=`, `>=`
|
|
252
|
+
- Identifiers and dotted column references only — no raw SQL, no functions, no subqueries
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
# Valid
|
|
256
|
+
postbase.from_("orders").join("users", on="orders.user_id = users.id")
|
|
257
|
+
|
|
258
|
+
# Invalid — rejected by the server
|
|
259
|
+
postbase.from_("orders").join("users", on="orders.user_id = users.id AND users.active = true")
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Column aliases** — when two joined tables share a column name (e.g. both have `id`), use `AS` to rename them. The SDK strips the alias before sending to the server and renames the keys in the returned rows client-side.
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
result = (
|
|
266
|
+
postbase.from_("apis")
|
|
267
|
+
.join("pricing_plans", on="apis.pricing_plan_id = pricing_plans.id", type="left")
|
|
268
|
+
.select("apis.id as api_id, apis.name, pricing_plans.id as plan_id, pricing_plans.name as plan_name")
|
|
269
|
+
.execute()
|
|
270
|
+
)
|
|
271
|
+
# result.data[0] == {"api_id": "...", "name": "...", "plan_id": "...", "plan_name": "..."}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
> **Limitation:** if you select two columns with the same base name without aliasing both (e.g. `apis.id, pricing_plans.id`), the server collapses them to one `id` key before the SDK sees the response — only one value survives. Always alias at least all but one of any colliding columns.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
### Raw SQL
|
|
279
|
+
|
|
280
|
+
For queries that can't be expressed with the builder (CTEs, window functions, complex aggregates), use `postbase.sql()`. RLS context is still enforced — the authenticated user's JWT is forwarded exactly as with `.from_()`.
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
result = postbase.sql(
|
|
284
|
+
"""
|
|
285
|
+
SELECT o.id, u.email
|
|
286
|
+
FROM orders o
|
|
287
|
+
INNER JOIN users u ON o.user_id = u.id
|
|
288
|
+
WHERE o.status = $1
|
|
289
|
+
""",
|
|
290
|
+
["active"],
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Multiple params
|
|
294
|
+
result = postbase.sql(
|
|
295
|
+
"""
|
|
296
|
+
SELECT p.title, COUNT(c.id) AS count
|
|
297
|
+
FROM posts p
|
|
298
|
+
LEFT JOIN comments c ON c.post_id = p.id
|
|
299
|
+
WHERE p.author_id = $1 AND p.status = $2
|
|
300
|
+
GROUP BY p.id, p.title
|
|
301
|
+
ORDER BY count DESC
|
|
302
|
+
LIMIT $3
|
|
303
|
+
""",
|
|
304
|
+
[user_id, "published", 10],
|
|
305
|
+
)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Params replace `$1`, `$2`, `$3`, … placeholders (standard PostgreSQL positional parameters). Never interpolate values directly into the query string — always use params to prevent SQL injection.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
### Insert
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
result = postbase.from_("posts").insert({"title": "Hello World", "status": "draft"}).select().single()
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Update
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
result = (
|
|
322
|
+
postbase.from_("posts")
|
|
323
|
+
.update({"status": "published"})
|
|
324
|
+
.eq("id", "post-id")
|
|
325
|
+
.select()
|
|
326
|
+
.single()
|
|
327
|
+
)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Upsert
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
result = (
|
|
334
|
+
postbase.from_("profiles")
|
|
335
|
+
.upsert({"id": "user-id", "username": "alice"}, on_conflict="id")
|
|
336
|
+
.select()
|
|
337
|
+
.execute()
|
|
338
|
+
)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Delete
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
result = postbase.from_("posts").delete().eq("id", "post-id").execute()
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Single row helpers
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
# Errors if not exactly one row
|
|
351
|
+
result = postbase.from_("posts").select("*").eq("id", post_id).single()
|
|
352
|
+
|
|
353
|
+
# Returns None if not found (no error)
|
|
354
|
+
result = postbase.from_("posts").select("*").eq("id", post_id).maybe_single()
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Pagination
|
|
358
|
+
|
|
359
|
+
```python
|
|
360
|
+
# Limit + offset
|
|
361
|
+
result = postbase.from_("posts").select("*").limit(20).offset(40).execute()
|
|
362
|
+
|
|
363
|
+
# Range (inclusive)
|
|
364
|
+
result = postbase.from_("posts").select("*").range(0, 19).execute()
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Authentication
|
|
370
|
+
|
|
371
|
+
### Sign up
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
response = postbase.auth.sign_up("user@example.com", "supersecret", remember_me=True)
|
|
375
|
+
# response.user, response.session, response.error
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Sign in with password
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
response = postbase.auth.sign_in_with_password("user@example.com", "supersecret", remember_me=True)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### OTP & Magic Link (passwordless)
|
|
385
|
+
|
|
386
|
+
**Magic link:**
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
postbase.auth.sign_in_with_otp("user@example.com", type="magic_link", redirect_to="https://yourapp.com/dashboard")
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**6-digit OTP code:**
|
|
393
|
+
|
|
394
|
+
```python
|
|
395
|
+
# 1. Request the code
|
|
396
|
+
postbase.auth.sign_in_with_otp("user@example.com", type="otp")
|
|
397
|
+
|
|
398
|
+
# 2. Verify the code
|
|
399
|
+
response = postbase.auth.verify_otp("user@example.com", "123456", remember_me=True)
|
|
400
|
+
# response.user, response.session
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Email OTP (the `/email-otp` flow)
|
|
404
|
+
|
|
405
|
+
```python
|
|
406
|
+
postbase.auth.sign_in_with_email_otp("user@example.com")
|
|
407
|
+
response = postbase.auth.verify_email_otp("user@example.com", "123456", remember_me=True)
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### OAuth (redirect-based, PKCE)
|
|
411
|
+
|
|
412
|
+
There's no browser in a Python backend to redirect for you — build the authorize URL, issue the HTTP redirect yourself in your framework's route handler, and persist `code_verifier` / `state` (e.g. in a server-side session) so you can complete the flow on callback:
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
oauth = postbase.auth.get_oauth_sign_in_url(
|
|
416
|
+
"google",
|
|
417
|
+
redirect_to="https://yourapp.com/auth/callback",
|
|
418
|
+
)
|
|
419
|
+
# oauth["url"] -> redirect the user here
|
|
420
|
+
# oauth["code_verifier"] -> stash in session/cookie, needed nowhere else since
|
|
421
|
+
# Postbase's server completes the PKCE exchange itself
|
|
422
|
+
# oauth["state"] -> CSRF token embedded in the redirect
|
|
423
|
+
|
|
424
|
+
# In your framework, return a redirect response to oauth["url"].
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Handle OAuth callback
|
|
428
|
+
|
|
429
|
+
Postbase's OAuth callback redirects back to your `redirect_to` URL with session tokens as query params. Pass the full callback URL your route handler received:
|
|
430
|
+
|
|
431
|
+
```python
|
|
432
|
+
# e.g. in a FastAPI/Flask/Django view for /auth/callback
|
|
433
|
+
response = postbase.auth.handle_oauth_callback(str(request.url))
|
|
434
|
+
# response.session, response.user, response.error
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Sign in with Apple / Google (native id_token — no browser)
|
|
438
|
+
|
|
439
|
+
For mobile/native apps that hand you an `id_token` directly from the platform SDK, skip the browser redirect entirely:
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
response = postbase.auth.sign_in_with_id_token(
|
|
443
|
+
provider="apple",
|
|
444
|
+
id_token=apple_identity_token,
|
|
445
|
+
nonce=nonce, # optional — include if you passed a nonce to the native request
|
|
446
|
+
remember_me=True,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
response = postbase.auth.sign_in_with_id_token(
|
|
450
|
+
provider="google",
|
|
451
|
+
id_token=google_id_token,
|
|
452
|
+
remember_me=True,
|
|
453
|
+
)
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
> **Note:** the provider must be enabled in your Postbase dashboard. The `clientId` field should contain your Apple Service ID (for web) or comma-separated Bundle IDs (for native), matching the `aud` claim in Apple's `id_token`.
|
|
457
|
+
|
|
458
|
+
### Remember me
|
|
459
|
+
|
|
460
|
+
`remember_me=True` issues a 30-day refresh token instead of the default 7-day one. The flag is stored on the session row server-side, so it's carried forward automatically on every subsequent `refresh_session()` call — no need to keep resending it.
|
|
461
|
+
|
|
462
|
+
Supported directly (single call, no follow-up needed) on `sign_up`, `sign_in_with_password`, `verify_otp`, `verify_email_otp`, and `sign_in_with_id_token`.
|
|
463
|
+
|
|
464
|
+
**Redirect-based OAuth is the one exception** — the tokens come back as URL query params on the callback, not from a call you control, so there's no request body to put `remember_me` in. Use `set_remember_me` afterwards instead:
|
|
465
|
+
|
|
466
|
+
```python
|
|
467
|
+
response = postbase.auth.set_remember_me(True)
|
|
468
|
+
# response.session.refresh_token is now valid for 30 days
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Get current user / session
|
|
472
|
+
|
|
473
|
+
```python
|
|
474
|
+
user_result = postbase.auth.get_user()
|
|
475
|
+
# user_result["data"]["user"], user_result["error"]
|
|
476
|
+
|
|
477
|
+
session_result = postbase.auth.get_session()
|
|
478
|
+
# session_result["data"]["session"], session_result["error"]
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
> `session.expires_at` is the **access token's** expiry (short-lived, ~1 hour). `session.refresh_token_expires_at` is the **refresh token's** expiry (7 or 30 days depending on `remember_me`) — this is what `set_session()` uses for the cookie's `max_age` in SSR contexts. Don't use `expires_at` to reason about how long the user stays logged in.
|
|
482
|
+
|
|
483
|
+
### Sign out
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
postbase.auth.sign_out()
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Update user
|
|
490
|
+
|
|
491
|
+
```python
|
|
492
|
+
postbase.auth.update_user(name="Alice", data={"plan": "pro"})
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Listen to auth state changes
|
|
496
|
+
|
|
497
|
+
```python
|
|
498
|
+
def on_change(event, session):
|
|
499
|
+
# event: "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED" | "USER_UPDATED"
|
|
500
|
+
print(event, session)
|
|
501
|
+
|
|
502
|
+
sub = postbase.auth.on_auth_state_change(on_change)
|
|
503
|
+
sub.unsubscribe()
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Admin (service role key required)
|
|
507
|
+
|
|
508
|
+
```python
|
|
509
|
+
admin_client = create_client(url, "pb_service_your_service_key", project_id="your-project-id")
|
|
510
|
+
|
|
511
|
+
# List users
|
|
512
|
+
result = admin_client.auth.admin.list_users(page=1, per_page=50)
|
|
513
|
+
|
|
514
|
+
# Create user
|
|
515
|
+
result = admin_client.auth.admin.create_user(
|
|
516
|
+
email="new@example.com",
|
|
517
|
+
password="password",
|
|
518
|
+
email_confirm=True,
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# Update / delete user
|
|
522
|
+
admin_client.auth.admin.update_user_by_id(user_id, email="new@example.com")
|
|
523
|
+
admin_client.auth.admin.delete_user(user_id)
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Storage
|
|
529
|
+
|
|
530
|
+
### Upload a file
|
|
531
|
+
|
|
532
|
+
Pass `content_type` to ensure the correct MIME type is stored with the file — required for binary uploads (PNG, PDF, etc.). Accepts raw `bytes` or any file-like object exposing `.read()`.
|
|
533
|
+
|
|
534
|
+
```python
|
|
535
|
+
with open("avatar.png", "rb") as f:
|
|
536
|
+
result = postbase.storage.from_("avatars").upload("user-123.png", f, content_type="image/png")
|
|
537
|
+
# result.data == {"path": "...", "fullPath": "..."}
|
|
538
|
+
|
|
539
|
+
# Or pass bytes directly
|
|
540
|
+
result = postbase.storage.from_("avatars").upload("user-123.png", image_bytes, content_type="image/png")
|
|
541
|
+
|
|
542
|
+
# Upsert (overwrite an existing file)
|
|
543
|
+
result = postbase.storage.from_("avatars").upload(
|
|
544
|
+
"user-123.png", image_bytes, content_type="image/png", upsert=True
|
|
545
|
+
)
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Get public URL
|
|
549
|
+
|
|
550
|
+
```python
|
|
551
|
+
result = postbase.storage.from_("avatars").get_public_url("user-123.png")
|
|
552
|
+
# result["data"]["publicUrl"]
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Download a file
|
|
556
|
+
|
|
557
|
+
```python
|
|
558
|
+
result = postbase.storage.from_("avatars").download("user-123.png")
|
|
559
|
+
# result["data"] is raw bytes
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Create a signed URL (temporary access)
|
|
563
|
+
|
|
564
|
+
```python
|
|
565
|
+
result = postbase.storage.from_("private-docs").create_signed_url("report.pdf", 3600) # 1 hour
|
|
566
|
+
# result.data["signedUrl"]
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### List files
|
|
570
|
+
|
|
571
|
+
```python
|
|
572
|
+
result = postbase.storage.from_("avatars").list("folder/", limit=100, sort_by_column="name")
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Delete files
|
|
576
|
+
|
|
577
|
+
```python
|
|
578
|
+
postbase.storage.from_("avatars").remove(["user-123.png", "user-456.png"])
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### Move / Copy
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
postbase.storage.from_("docs").move("old-name.pdf", "new-name.pdf")
|
|
585
|
+
postbase.storage.from_("docs").copy("template.pdf", "copy.pdf")
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Bucket management
|
|
589
|
+
|
|
590
|
+
```python
|
|
591
|
+
# Create
|
|
592
|
+
postbase.storage.create_bucket(
|
|
593
|
+
"avatars",
|
|
594
|
+
public=True,
|
|
595
|
+
file_size_limit=5 * 1024 * 1024, # 5 MB
|
|
596
|
+
allowed_mime_types=["image/png", "image/jpeg"],
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# List
|
|
600
|
+
buckets = postbase.storage.list_buckets()
|
|
601
|
+
|
|
602
|
+
# Update
|
|
603
|
+
postbase.storage.update_bucket("avatars", public=False)
|
|
604
|
+
|
|
605
|
+
# Delete
|
|
606
|
+
postbase.storage.delete_bucket("avatars")
|
|
607
|
+
|
|
608
|
+
# Empty (delete all objects)
|
|
609
|
+
postbase.storage.empty_bucket("avatars")
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## RPC (PostgreSQL functions)
|
|
615
|
+
|
|
616
|
+
Call a stored procedure or function in your project's schema:
|
|
617
|
+
|
|
618
|
+
```python
|
|
619
|
+
result = postbase.rpc("get_nearby_posts", {"lat": 37.7749, "lng": -122.4194, "radius": 10})
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## Email
|
|
625
|
+
|
|
626
|
+
Send a transactional email using your project's configured email provider (e.g. AWS SES).
|
|
627
|
+
|
|
628
|
+
```python
|
|
629
|
+
result = postbase.email.send(
|
|
630
|
+
to="user@example.com",
|
|
631
|
+
subject="Welcome!",
|
|
632
|
+
text="Hello there",
|
|
633
|
+
html="<p>Hello there</p>",
|
|
634
|
+
reply_to="support@example.com", # optional
|
|
635
|
+
)
|
|
636
|
+
# result["data"]["ok"]
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## SSR / server-side session forwarding
|
|
642
|
+
|
|
643
|
+
When running behind a web framework (FastAPI, Flask, Django, etc.), you can forward the caller's session cookie to Postbase so RLS policies evaluate against the authenticated user instead of just the anon role. Implement a `CookieAdapter` bridging your framework's request/response to Postbase:
|
|
644
|
+
|
|
645
|
+
```python
|
|
646
|
+
from postbase import CookieAdapter, Cookie, create_client
|
|
647
|
+
|
|
648
|
+
def get_all():
|
|
649
|
+
# Read cookies off the incoming request (framework-specific)
|
|
650
|
+
return [Cookie(name=name, value=value) for name, value in request.cookies.items()]
|
|
651
|
+
|
|
652
|
+
def set_all(cookies_to_set):
|
|
653
|
+
# Write cookies onto the outgoing response (framework-specific)
|
|
654
|
+
for c in cookies_to_set:
|
|
655
|
+
response.set_cookie(c.name, c.value, **c.options)
|
|
656
|
+
|
|
657
|
+
postbase = create_client(
|
|
658
|
+
url, anon_key,
|
|
659
|
+
project_id=project_id,
|
|
660
|
+
cookies=CookieAdapter(get_all=get_all, set_all=set_all),
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
result = postbase.from_("posts").select().execute() # RLS applies to the signed-in user
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
`get_all`/`set_all` may be sync or async callables — `AsyncClient` awaits them automatically if they return an awaitable; the sync `Client` requires plain (non-async) callables.
|
|
667
|
+
|
|
668
|
+
The session cookie is named `postbase-session`. After completing an OAuth flow or otherwise obtaining a session outside the normal sign-in calls, persist it with `auth.set_session(session)` — this writes the `postbase-session` httpOnly cookie via your `CookieAdapter.set_all`, so subsequent requests using the same adapter are authenticated automatically.
|
|
669
|
+
|
|
670
|
+
```python
|
|
671
|
+
error = postbase.auth.set_session(session)["error"]
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
## Row Level Security (RLS)
|
|
677
|
+
|
|
678
|
+
When a user is signed in (via a forwarded `X-Postbase-Token`/session cookie), their session JWT is automatically forwarded with every query. Your RLS policies can reference the user via:
|
|
679
|
+
|
|
680
|
+
```sql
|
|
681
|
+
current_setting('postbase.user_id', true) -- the authenticated user's ID
|
|
682
|
+
current_setting('postbase.role', true) -- the user's role
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Example policy — users can only read their own rows:
|
|
686
|
+
|
|
687
|
+
```sql
|
|
688
|
+
CREATE POLICY "own rows" ON posts
|
|
689
|
+
FOR SELECT USING (
|
|
690
|
+
user_id = current_setting('postbase.user_id', true)::uuid
|
|
691
|
+
);
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
## Environment Variables
|
|
697
|
+
|
|
698
|
+
We recommend storing your Postbase credentials in environment variables:
|
|
699
|
+
|
|
700
|
+
```bash
|
|
701
|
+
POSTBASE_URL=https://your-postbase-instance.com
|
|
702
|
+
POSTBASE_ANON_KEY=pb_anon_...
|
|
703
|
+
POSTBASE_PROJECT_ID=your-project-id
|
|
704
|
+
# Service key — server-side only, bypasses RLS
|
|
705
|
+
POSTBASE_SERVICE_KEY=pb_service_...
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
Use your **service role key** (`pb_service_...`) only in trusted server-side code — it bypasses RLS.
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## Sync vs. async
|
|
713
|
+
|
|
714
|
+
| | `postbase` | `postbase.aio` |
|
|
715
|
+
|---|---|---|
|
|
716
|
+
| Client factory | `create_client(...)` | `create_async_client(...)` |
|
|
717
|
+
| HTTP backend | `httpx.Client` | `httpx.AsyncClient` |
|
|
718
|
+
| Call style | `result = postbase.from_("t").select().execute()` | `result = await postbase.from_("t").select().execute()` |
|
|
719
|
+
| Awaiting a builder directly | not supported — call `.execute()` | `await postbase.from_("t").select()` implicitly executes |
|
|
720
|
+
| Context manager | `with create_client(...) as postbase:` | `async with create_async_client(...) as postbase:` |
|
|
721
|
+
|
|
722
|
+
Both share the same method names, arguments, and return shapes (`QueryResult`, `SingleResult`, `AuthResponse`, dataclasses) — only sync/async mechanics differ.
|
|
723
|
+
|
|
724
|
+
---
|
|
725
|
+
|
|
726
|
+
## Type hints
|
|
727
|
+
|
|
728
|
+
The SDK is fully type-annotated. Query results are `QueryResult[T]` / `SingleResult[T]` dataclasses:
|
|
729
|
+
|
|
730
|
+
```python
|
|
731
|
+
from dataclasses import dataclass
|
|
732
|
+
from postbase import create_client
|
|
733
|
+
|
|
734
|
+
@dataclass
|
|
735
|
+
class Post:
|
|
736
|
+
id: str
|
|
737
|
+
title: str
|
|
738
|
+
status: str
|
|
739
|
+
created_at: str
|
|
740
|
+
|
|
741
|
+
postbase = create_client(url, key, project_id=project_id)
|
|
742
|
+
result = postbase.from_("posts").select().eq("status", "published").execute()
|
|
743
|
+
# result.data is a list[dict] — construct your dataclass from each row as needed:
|
|
744
|
+
posts = [Post(**row) for row in (result.data or [])]
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
749
|
+
## License
|
|
750
|
+
|
|
751
|
+
MIT — see [LICENSE](LICENSE).
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
Built with love by the [Postbase](https://www.getpostbase.com) team.
|