@motherbase/client 1.5.2 → 2.0.2
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.
- package/LICENSE +21 -0
- package/README.md +382 -386
- package/dist/auth.d.ts +147 -428
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +287 -850
- package/dist/auth.js.map +1 -1
- package/dist/cookie.d.ts.map +1 -1
- package/dist/cookie.js +11 -1
- package/dist/cookie.js.map +1 -1
- package/dist/database.d.ts +2 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/devices.d.ts +13 -0
- package/dist/devices.d.ts.map +1 -0
- package/dist/devices.js +13 -0
- package/dist/devices.js.map +1 -0
- package/dist/http.d.ts +7 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +27 -24
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +12 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -12
- package/dist/index.js.map +1 -1
- package/dist/pkce.d.ts.map +1 -1
- package/dist/pkce.js +27 -4
- package/dist/pkce.js.map +1 -1
- package/dist/postgrest.d.ts +7 -0
- package/dist/postgrest.d.ts.map +1 -1
- package/dist/postgrest.js +11 -1
- package/dist/postgrest.js.map +1 -1
- package/dist/profile.d.ts +16 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +26 -0
- package/dist/profile.js.map +1 -0
- package/dist/realtime.d.ts +3 -0
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +20 -3
- package/dist/realtime.js.map +1 -1
- package/dist/session.d.ts +2 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +26 -10
- package/dist/session.js.map +1 -1
- package/package.json +76 -62
package/README.md
CHANGED
|
@@ -1,307 +1,302 @@
|
|
|
1
|
-
# @motherbase/client
|
|
2
|
-
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
`.channel()`, `.functions`) and the same
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
(`client.auth.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
`
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
client
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
##
|
|
89
|
-
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
const { data:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
client.from(table).
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
//
|
|
186
|
-
client.
|
|
187
|
-
client.
|
|
188
|
-
client.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
client.
|
|
194
|
-
|
|
195
|
-
client.auth.
|
|
196
|
-
|
|
197
|
-
client.auth.
|
|
198
|
-
client.auth.
|
|
199
|
-
client.auth.
|
|
200
|
-
client.auth.
|
|
201
|
-
client.auth.
|
|
202
|
-
client.auth.
|
|
203
|
-
client.auth.
|
|
204
|
-
client.auth.
|
|
205
|
-
client.auth.
|
|
206
|
-
client.auth.
|
|
207
|
-
client.auth.
|
|
208
|
-
client.auth.
|
|
209
|
-
client.auth.
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
client.auth.
|
|
214
|
-
client.auth.
|
|
215
|
-
client.auth.
|
|
216
|
-
client.auth.
|
|
217
|
-
client.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
client.
|
|
222
|
-
client.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
client.storage.from(bucket).
|
|
227
|
-
client.storage.from(bucket).
|
|
228
|
-
client.storage.from(bucket).
|
|
229
|
-
client.storage.from(bucket).
|
|
230
|
-
client.storage.from(bucket).
|
|
231
|
-
client.storage.from(bucket).
|
|
232
|
-
client.storage.from(bucket).
|
|
233
|
-
client.storage.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
client.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (previous[0]) upload.resumeFromPreviousUpload(previous[0]);
|
|
284
|
-
upload.start();
|
|
285
|
-
});
|
|
286
|
-
```
|
|
287
|
-
|
|
1
|
+
# @motherbase/client
|
|
2
|
+
|
|
3
|
+
A client SDK for the Motherbase self-hosted backend. The data, storage,
|
|
4
|
+
realtime and functions layers are **supabase-js-shaped**: the same entry points
|
|
5
|
+
(`createClient`, `.from()`, `.storage`, `.channel()`, `.functions`) and the same
|
|
6
|
+
`{ data, error }` result convention.
|
|
7
|
+
|
|
8
|
+
**Auth is Better Auth, on purpose.** `client.auth` is
|
|
9
|
+
[Better Auth](https://www.better-auth.com/docs)'s own client, typed with every
|
|
10
|
+
plugin the server mounts, served through this package (you do not install
|
|
11
|
+
`better-auth` yourself). Every method in Better Auth's documentation is there
|
|
12
|
+
under its own name: `client.auth.signIn.email()`, `client.auth.listSessions()`,
|
|
13
|
+
`client.auth.revokeSession()`, `client.auth.twoFactor.enable()`,
|
|
14
|
+
`client.auth.organization.create()`, `client.auth.passkey.addPasskey()`… We
|
|
15
|
+
chose it over a supabase-js imitation because it exposes more: sessions per
|
|
16
|
+
device, organizations and teams, passkeys, SSO, API keys. Porting supabase-js
|
|
17
|
+
auth code is a rename, and [`COMPATIBILITY.md`](./COMPATIBILITY.md) names the
|
|
18
|
+
Better Auth call for every supabase-js one (`motherbase migrate codemod` lists
|
|
19
|
+
them in your code).
|
|
20
|
+
|
|
21
|
+
**Scope, honestly.** For the **data layer**, this is a package swap: change the
|
|
22
|
+
import and the client factory, keep your queries. Every `supabase-js` data call
|
|
23
|
+
in the contract suite is either supported or supported with a stated difference,
|
|
24
|
+
including relational embedding (`select('*, author(*)')`), `.rpc()`, native
|
|
25
|
+
`upsert`, `count`, `csv()`, the full filter set and PostgREST error codes.
|
|
26
|
+
Realtime is close, with named gaps (no private channels).
|
|
27
|
+
|
|
28
|
+
**Storage is path-addressed too**: `upload('avatars/123/pic.png')` then
|
|
29
|
+
`download('avatars/123/pic.png')` round trips, `list(prefix)` works, and keys are
|
|
30
|
+
traversal-checked. `copy`, `move`, `info`, `exists`, batch signed URLs and
|
|
31
|
+
direct-to-storage signed uploads are supported; image transforms are not.
|
|
32
|
+
Authorization is ownership plus bucket tokens by default, and real RLS policies on
|
|
33
|
+
`storage.files` when the `storage.policies` setting is on. See
|
|
34
|
+
[`docs/migrating-from-supabase.md`](../../docs/migrating-from-supabase.md).
|
|
35
|
+
|
|
36
|
+
On the server, `createClient(url, secretKey)` also authorizes storage objects as
|
|
37
|
+
`service_role`, including objects uploaded by another user. Secret-key
|
|
38
|
+
impersonation keeps the permissions of the impersonated caller. Anonymous uploads
|
|
39
|
+
require storage policies granting INSERT; they do not require SELECT. An upload
|
|
40
|
+
response's `url` can be `null` when the caller cannot read the object.
|
|
41
|
+
|
|
42
|
+
Uploads accept `Blob`, `ArrayBuffer`, and typed arrays, including Node `Buffer`.
|
|
43
|
+
Signed uploads carry a scoped finalization token; keep the entire returned token
|
|
44
|
+
when calling `uploadToSignedUrl`. This capability grants finalization, not private
|
|
45
|
+
read access. Public URLs use the shared contracts route.
|
|
46
|
+
|
|
47
|
+
For auth mutations in Node, the SDK supplies the project URL's origin when no
|
|
48
|
+
`Origin` header was provided. An explicit `headers: { Origin: 'https://app.example' }`
|
|
49
|
+
is preserved and must be trusted by the server. Browser requests retain the
|
|
50
|
+
browser's Origin and the server's CSRF checks remain enabled.
|
|
51
|
+
|
|
52
|
+
Do not plan a migration from the paragraph above. Plan it from
|
|
53
|
+
[`COMPATIBILITY.md`](./COMPATIBILITY.md), which is generated from contract tests
|
|
54
|
+
and is exhaustive, per method, with the exact difference for every partial row.
|
|
55
|
+
|
|
56
|
+
**Write responses (Supabase semantics).** Native and compatibility clients,
|
|
57
|
+
including SSR clients, return `data: null` after a successful write unless you
|
|
58
|
+
chain `.select()`. For a public contact form, grant INSERT and create an INSERT
|
|
59
|
+
policy; no SELECT policy is needed. Requesting rows also requires SELECT access.
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// Write only: no SELECT access required.
|
|
63
|
+
const { error } = await client.from('contacts').insert({ email, message });
|
|
64
|
+
|
|
65
|
+
// Read back the stored row, including generated defaults, if RLS permits it.
|
|
66
|
+
const { data } = await client.from('contacts').insert({ email, message }).select().single();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Existing Motherbase code that consumed implicit write responses must add
|
|
70
|
+
`.select()` to those calls. Raw REST writes default to `Prefer: return=minimal`;
|
|
71
|
+
use `Prefer: return=representation` to receive rows. Apply the
|
|
72
|
+
`20260924120000_rest_write_capture` server migration before starting the updated
|
|
73
|
+
server: internal trigger capture keeps realtime/webhooks transactional without
|
|
74
|
+
requiring client read access. It does not enable event delivery for arbitrary
|
|
75
|
+
SQL-editor or RPC writes. Motherbase's `transaction()` extension retains its
|
|
76
|
+
existing batch response contract.
|
|
77
|
+
|
|
78
|
+
**Invalid keys are refused.** A project key that is unknown or revoked now
|
|
79
|
+
answers `401 UNAUTHORIZED` even when the project does not require an API key.
|
|
80
|
+
Before, it was ignored and the request ran as anonymous. Remove stale keys from
|
|
81
|
+
client configuration rather than relying on that fallback.
|
|
82
|
+
|
|
83
|
+
Typed, isomorphic (browser, Next.js server & client, Expo / React Native), built
|
|
84
|
+
on global `fetch`. Its runtime dependencies are Better Auth's client and its
|
|
85
|
+
plugins; `socket.io-client` is an optional peer dependency, used only for
|
|
86
|
+
realtime.
|
|
87
|
+
|
|
88
|
+
## Install
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm i @motherbase/client
|
|
92
|
+
# optional, only if you use realtime:
|
|
93
|
+
npm i socket.io-client
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Quickstart
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { createClient, localStorageStorage } from '@motherbase/client';
|
|
100
|
+
|
|
101
|
+
const client = createClient(
|
|
102
|
+
'http://localhost:5001',
|
|
103
|
+
'your-anon-key', // optional; sent as an `apikey` header
|
|
104
|
+
{ storage: localStorageStorage() } // omit for in-memory (SSR-safe) sessions
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
await client.hydrate(); // restore a persisted session (optional)
|
|
108
|
+
|
|
109
|
+
// Auth (Better Auth's client)
|
|
110
|
+
await client.auth.signUp.email({ email: 'me@example.com', password: 'secret123', name: 'Me' });
|
|
111
|
+
const { data, error } = await client.auth.signIn.email({
|
|
112
|
+
email: 'me@example.com',
|
|
113
|
+
password: 'secret123',
|
|
114
|
+
});
|
|
115
|
+
const { data: devices } = await client.auth.listSessions();
|
|
116
|
+
|
|
117
|
+
// Query builder (awaitable, PostgREST-style)
|
|
118
|
+
const { data: products } = await client
|
|
119
|
+
.from('products')
|
|
120
|
+
.select('id,name,price')
|
|
121
|
+
.lt('price', 50)
|
|
122
|
+
.order('name', { ascending: false })
|
|
123
|
+
.limit(20);
|
|
124
|
+
|
|
125
|
+
// Storage. `uploaded.path` is the key you passed, so later download/remove/
|
|
126
|
+
// signed-URL calls take the same string.
|
|
127
|
+
const { data: uploaded } = await client.storage.from('avatars').upload(`${userId}/me.png`, myBlob, {
|
|
128
|
+
visibility: 'public',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Realtime: postgres_changes, broadcast, presence over one channel (Supabase-shaped)
|
|
132
|
+
const room = client
|
|
133
|
+
.channel('room-1', { config: { broadcast: { self: true }, presence: { key: userId } } })
|
|
134
|
+
.on('postgres_changes', { event: '*', schema: 'public', table: 'messages', filter: 'room_id=eq.42' }, p => {
|
|
135
|
+
console.log(p.eventType, p.new, p.old);
|
|
136
|
+
})
|
|
137
|
+
.on('broadcast', { event: 'cursor' }, ({ payload }) => console.log(payload))
|
|
138
|
+
.on('presence', { event: 'sync' }, () => console.log(room.presenceState()))
|
|
139
|
+
.subscribe();
|
|
140
|
+
|
|
141
|
+
await room.send({ type: 'broadcast', event: 'cursor', payload: { x: 1, y: 2 } });
|
|
142
|
+
await room.track({ online_at: Date.now() });
|
|
143
|
+
|
|
144
|
+
// Edge functions
|
|
145
|
+
const { data: result } = await client.functions.invoke('hello', { body: { name: 'Ada' } });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Every data/auth/storage call resolves to `{ data, error }` and **never throws**
|
|
149
|
+
for expected API errors. `error` is `null` on success, otherwise
|
|
150
|
+
`{ message, status?, code? }` (Better Auth's `{ message?, status, statusText }`
|
|
151
|
+
for `client.auth`).
|
|
152
|
+
|
|
153
|
+
The SDK attaches the access token automatically and revalidates it on a 401
|
|
154
|
+
(single-flight: concurrent requests share one revalidation). Sessions are Better
|
|
155
|
+
Auth bearer sessions, so there is no separate refresh token to rotate: when the
|
|
156
|
+
token is no longer valid the SDK clears the session and emits `SIGNED_OUT`.
|
|
157
|
+
|
|
158
|
+
## API surface (cheatsheet)
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// Data. Filters: eq neq gt gte lt lte like ilike likeAllOf likeAnyOf ilikeAllOf
|
|
162
|
+
// ilikeAnyOf in is not or filter match textSearch contains containedBy overlaps
|
|
163
|
+
// rangeGt rangeGte rangeLt rangeLte rangeAdjacent
|
|
164
|
+
client.from(table).select(cols).eq(col, v).in(col, [..]).order(col, { ascending, nullsFirst, referencedTable })
|
|
165
|
+
client.from(table).select(cols).limit(n, { referencedTable }).range(from, to).abortSignal(signal)
|
|
166
|
+
client.from(table).select(cols).or('a.eq.1,and(b.gt.2,c.is.null)') // nests, as PostgREST does
|
|
167
|
+
client.from(table).select(cols).not(col, 'eq', v).filter(col, 'isdistinct', v)
|
|
168
|
+
client.from(table).select(cols).textSearch(col, q, { type: 'websearch', config: 'english' })
|
|
169
|
+
client.from(table).select(cols).single() | .maybeSingle() | .returns<T>() | .csv() | .geojson() | .explain()
|
|
170
|
+
client.from(table).update(v).eq(..).throwOnError() | .maxAffected(1) | .rollback()
|
|
171
|
+
client.from(table).insert(row) | .insert([rowA, rowB], { defaultToNull: false })
|
|
172
|
+
client.from(table).upsert(rows, { onConflict, ignoreDuplicates })
|
|
173
|
+
client.from(table).update(values).eq(..) // a filter is required
|
|
174
|
+
client.from(table).delete().eq(..) // a filter is required
|
|
175
|
+
// Every call resolves { data, error, count }. `count` is null unless you ask:
|
|
176
|
+
// .select('*', { count: 'exact', head: true }).
|
|
177
|
+
// Values are quoted for you: .in('name', ['a,b']) is one value, not two.
|
|
178
|
+
// Writes return { data: null, error: null } by default, like supabase-js.
|
|
179
|
+
// Add .select() to insert/upsert/update/delete to request the written rows.
|
|
180
|
+
// Returning rows requires a SELECT policy as well as the write policy.
|
|
181
|
+
// An unbounded select is capped by api.rest.maxRows (1000 by default): paginate with .range().
|
|
182
|
+
// explain() needs an admin caller; a normal caller gets 403.
|
|
183
|
+
|
|
184
|
+
// Relational embedding. Compiles to a correlated LATERAL subquery inside the
|
|
185
|
+
// same RLS transaction, so embedded rows are policy-filtered too.
|
|
186
|
+
client.from('posts').select('id, author(name), comments(id)') // object / array by FK shape
|
|
187
|
+
client.from('posts').select('id, author!inner(name)') // inner join semantics
|
|
188
|
+
client.from('posts').select('id, comments(count)') // aggregate
|
|
189
|
+
|
|
190
|
+
// Database functions (typed from the generated Database["public"]["Functions"]).
|
|
191
|
+
// Runs in the caller's RLS transaction. SECURITY DEFINER functions are refused
|
|
192
|
+
// unless an operator allowlists them.
|
|
193
|
+
client.rpc(fn, args, { get, head, count })
|
|
194
|
+
|
|
195
|
+
// Auth → Better Auth's client: https://www.better-auth.com/docs. A sample of
|
|
196
|
+
// what is there; each plugin needs to be on in the project's Auth settings.
|
|
197
|
+
client.auth.signUp.email({ email, password, name })
|
|
198
|
+
client.auth.signIn.email({ email, password }) | .social({ provider, callbackURL }) | .anonymous()
|
|
199
|
+
client.auth.signIn.emailOtp({ email, otp }) | .magicLink({ email }) | .passkey() | .sso({ email, callbackURL })
|
|
200
|
+
client.auth.getSession() // { session, user }, asks the server
|
|
201
|
+
client.auth.signOut()
|
|
202
|
+
client.auth.listSessions() | .revokeSession({ token }) | .revokeOtherSessions() | .revokeSessions()
|
|
203
|
+
client.auth.updateUser({ name, image }) | .changePassword({ currentPassword, newPassword }) | .deleteUser({ password })
|
|
204
|
+
client.auth.listAccounts() | .linkSocial({ provider, callbackURL }) | .unlinkAccount({ providerId })
|
|
205
|
+
client.auth.emailOtp.sendVerificationOtp({ email, type }) | .requestPasswordReset({ email }) | .resetPassword({ email, otp, password })
|
|
206
|
+
client.auth.twoFactor.enable({ password }) | .verifyTotp({ code }) | .verifyOtp({ code }) | .disable({ password })
|
|
207
|
+
client.auth.organization.create({ name, slug }) | .list() | .setActive({ organizationId }) | .inviteMember({ email, role })
|
|
208
|
+
client.auth.passkey.addPasskey() | .listUserPasskeys() | .deletePasskey({ id })
|
|
209
|
+
client.auth.sso.register(...) | client.auth.apiKey.create(...) | client.auth.token() // a JWT for the session
|
|
210
|
+
|
|
211
|
+
// Auth → what Motherbase adds to it
|
|
212
|
+
client.auth.onAuthStateChange((event, session) => {}) // INITIAL_SESSION | SIGNED_IN | USER_UPDATED | SIGNED_OUT
|
|
213
|
+
client.auth.getSessionFromUrl() // finishes an OAuth redirect, see below
|
|
214
|
+
client.auth.exchangeCodeForSession(code) // the same with flowType: 'pkce'
|
|
215
|
+
client.auth.reauthenticate() | .updateUserCredentials({ email, password, nonce })
|
|
216
|
+
client.auth.admin.* // secret key only: list/get/create/update/delete/signOut/invite
|
|
217
|
+
client.hydrate() // the stored session, no network call: display only
|
|
218
|
+
|
|
219
|
+
// Storage → { data, error }. `path` is the object key inside the bucket, and it
|
|
220
|
+
// round trips. The file id returned by upload still works anywhere a path does.
|
|
221
|
+
client.storage.from(bucket).upload(path, file, { visibility, upsert })
|
|
222
|
+
client.storage.from(bucket).download(path, { transform }) // transform: see below
|
|
223
|
+
client.storage.from(bucket).remove([path])
|
|
224
|
+
client.storage.from(bucket).list(prefix, { limit, offset, search, sortBy })
|
|
225
|
+
client.storage.from(bucket).createSignedUrl(path, expiresIn, { transform }) // seconds, 1..604800
|
|
226
|
+
client.storage.from(bucket).createSignedUrls([path], expiresIn) // per-entry results
|
|
227
|
+
client.storage.from(bucket).getPublicUrl(path, { transform }) // sync
|
|
228
|
+
client.storage.from(bucket).update(path, file) // upload with upsert on
|
|
229
|
+
client.storage.from(bucket).copy(from, to, { upsert }) | .move(from, to, { upsert })
|
|
230
|
+
client.storage.from(bucket).info(path) | .exists(path) // 404 reads as false
|
|
231
|
+
client.storage.from(bucket).createSignedUploadUrl(path, { size, contentType }) // S3 driver
|
|
232
|
+
client.storage.from(bucket).uploadToSignedUrl(path, token, file)
|
|
233
|
+
client.storage.createBucket(id, { public, fileSizeLimit, allowedMimeTypes }) // secret key
|
|
234
|
+
// transform: { width, height, resize: 'cover'|'contain'|'fill', quality, format: 'origin'|'webp'|'avif' }
|
|
235
|
+
|
|
236
|
+
// Realtime
|
|
237
|
+
client.channel(name, { config: { broadcast: { self, ack }, presence: { key } } })
|
|
238
|
+
.on('postgres_changes', { event, schema, table, filter }, cb)
|
|
239
|
+
.on('broadcast', { event }, cb)
|
|
240
|
+
.on('presence', { event: 'sync' | 'join' | 'leave' }, cb)
|
|
241
|
+
.subscribe(status => {}) // SUBSCRIBED | TIMED_OUT | CLOSED | CHANNEL_ERROR
|
|
242
|
+
channel.send({ type: 'broadcast', event, payload })
|
|
243
|
+
channel.track(state); channel.untrack(); channel.presenceState()
|
|
244
|
+
client.removeChannel(channel)
|
|
245
|
+
|
|
246
|
+
// Edge functions → { data, error }. `error` is a FunctionsHttpError /
|
|
247
|
+
// FunctionsFetchError instance, but invoke still resolves rather than throws.
|
|
248
|
+
client.functions.invoke(name, { body, method, headers, auth })
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Resumable uploads
|
|
252
|
+
|
|
253
|
+
Large files go over TUS, exactly as on Supabase: the SDK does not bundle a TUS
|
|
254
|
+
client (supabase-js does not either), so use
|
|
255
|
+
[`tus-js-client`](https://github.com/tus/tus-js-client) against
|
|
256
|
+
`/storage/v1/upload/resumable`. Chunks must be 6 MB. Bucket limits, the key
|
|
257
|
+
collision check and storage policies are enforced when the upload is created,
|
|
258
|
+
and an unfinished upload can be resumed for 24 hours.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { Upload } from 'tus-js-client';
|
|
262
|
+
|
|
263
|
+
const session = await client.hydrate();
|
|
264
|
+
const upload = new Upload(file, {
|
|
265
|
+
endpoint: `${MOTHERBASE_URL}/storage/v1/upload/resumable`,
|
|
266
|
+
chunkSize: 6 * 1024 * 1024,
|
|
267
|
+
retryDelays: [0, 3000, 5000, 10000, 20000],
|
|
268
|
+
headers: {
|
|
269
|
+
authorization: `Bearer ${session?.access_token}`,
|
|
270
|
+
apikey: MOTHERBASE_PUBLISHABLE_KEY,
|
|
271
|
+
'x-upsert': 'true', // optional: replace an existing object
|
|
272
|
+
},
|
|
273
|
+
metadata: { bucketName: 'videos', objectName: 'u1/clip.mp4', contentType: 'video/mp4' },
|
|
274
|
+
onProgress: (sent, total) => console.log(Math.round((sent / total) * 100), '%'),
|
|
275
|
+
onSuccess: () => console.log('uploaded'),
|
|
276
|
+
});
|
|
277
|
+
upload.findPreviousUploads().then(previous => {
|
|
278
|
+
if (previous[0]) upload.resumeFromPreviousUpload(previous[0]);
|
|
279
|
+
upload.start();
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
288
283
|
## Password sign-in with MFA
|
|
289
284
|
|
|
290
|
-
When the account requires a second factor, password sign-in
|
|
291
|
-
`
|
|
292
|
-
|
|
285
|
+
When the account requires a second factor, password sign-in answers
|
|
286
|
+
`twoFactorRedirect: true` and the available `twoFactorMethods`. No session is
|
|
287
|
+
stored until verification succeeds.
|
|
293
288
|
|
|
294
289
|
```ts
|
|
295
|
-
const { data, error } = await client.auth.
|
|
290
|
+
const { data, error } = await client.auth.signIn.email({ email, password });
|
|
296
291
|
if (error) throw new Error(error.message);
|
|
297
292
|
|
|
298
|
-
if (data
|
|
299
|
-
// Show a code field. For an
|
|
300
|
-
const { error: verifyError } = await client.auth.
|
|
293
|
+
if (data && 'twoFactorRedirect' in data) {
|
|
294
|
+
// Show a code field. For an emailed or texted code, call twoFactor.sendOtp() first.
|
|
295
|
+
const { error: verifyError } = await client.auth.twoFactor.verifyTotp({ code });
|
|
301
296
|
if (verifyError) throw new Error(verifyError.message);
|
|
302
297
|
}
|
|
303
298
|
|
|
304
|
-
// The verified session is
|
|
299
|
+
// The verified session is stored; onAuthStateChange reports MFA_CHALLENGE_VERIFIED.
|
|
305
300
|
```
|
|
306
301
|
|
|
307
302
|
The SDK includes the challenge cookie on sign-in and verification requests.
|
|
@@ -310,94 +305,95 @@ app and API are on different sites. A UI toggle cannot bypass a second factor
|
|
|
310
305
|
required by the server.
|
|
311
306
|
|
|
312
307
|
## Social sign-in (OAuth)
|
|
313
|
-
|
|
314
|
-
A provider callback is a browser redirect, so it cannot answer the SDK with a
|
|
315
|
-
session the way a password sign-in does. Two calls, not one:
|
|
316
|
-
|
|
317
|
-
```ts
|
|
318
|
-
// 1. Wherever the button lives. Navigates the browser to the backend, which
|
|
319
|
-
// redirects on to the provider.
|
|
320
|
-
await client.auth.
|
|
321
|
-
provider: 'google',
|
|
322
|
-
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
// 2. On that page
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
|
360
|
-
|
|
|
361
|
-
|
|
|
362
|
-
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
import
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
`
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
project
|
|
308
|
+
|
|
309
|
+
A provider callback is a browser redirect, so it cannot answer the SDK with a
|
|
310
|
+
session the way a password sign-in does. Two calls, not one:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
// 1. Wherever the button lives. Navigates the browser to the backend, which
|
|
314
|
+
// redirects on to the provider.
|
|
315
|
+
await client.auth.signIn.social({
|
|
316
|
+
provider: 'google',
|
|
317
|
+
callbackURL: 'https://app.example.com/auth/callback',
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// 2. On that page. Reads the token the backend appended to the redirect,
|
|
321
|
+
// stores the session, and strips it from the address bar. createClient
|
|
322
|
+
// already does this on load unless auth.detectSessionInUrl is false.
|
|
323
|
+
const { data, error } = await client.auth.getSessionFromUrl();
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`getSessionFromUrl()` resolves with `data: null` and no error when there is
|
|
327
|
+
nothing to read, so calling it on every page load is safe.
|
|
328
|
+
|
|
329
|
+
Step 1 is a full-page navigation, not a fetch. That is deliberate: the backend
|
|
330
|
+
sets an OAuth state cookie there and re-checks it on the provider callback, and
|
|
331
|
+
a cookie set on a cross-site fetch response is refused by Safari and Firefox and
|
|
332
|
+
by Chrome in incognito. Navigating makes it a first-party cookie, so the flow
|
|
333
|
+
works in every browser even when your app and your API are on different domains.
|
|
334
|
+
If you are not using the SDK, point the button at the same URL:
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
https://<your-api>/api/auth/sign-in/social/google?callbackURL=https://app.example.com/auth/callback
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`callbackURL` is validated against the trusted origins, so it has to be an
|
|
341
|
+
origin you allowlisted.
|
|
342
|
+
|
|
343
|
+
Two backend settings decide whether this works at all:
|
|
344
|
+
|
|
345
|
+
- **Callback URL.** Studio, Authentication, Sign In / Providers shows the exact
|
|
346
|
+
URL to register in the provider's console.
|
|
347
|
+
- **CORS allowed origins.** Studio, Authentication settings, Security · CORS.
|
|
348
|
+
If your app is served from a different origin than the API, that origin must
|
|
349
|
+
be listed. Otherwise the browser refuses the cookie the sign-in start sets,
|
|
350
|
+
and the provider callback comes back as `?error=state_mismatch`.
|
|
351
|
+
|
|
352
|
+
## Storage adapters
|
|
353
|
+
|
|
354
|
+
| Runtime | Adapter |
|
|
355
|
+
| -------- | ------------------------------------------------------------- |
|
|
356
|
+
| Browser | `localStorageStorage()` or `cookieStorage()` |
|
|
357
|
+
| SSR/Node | `memoryStorage()` (default), `serverCookieStorage(cookieMethods)` (getAll/setAll, see `@motherbase/ssr` for Next.js helpers), or a custom `{ get, set }` adapter |
|
|
358
|
+
| Expo/RN | custom `{ get, set }` backed by `AsyncStorage` |
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
362
|
+
import { createClient } from '@motherbase/client';
|
|
363
|
+
|
|
364
|
+
const client = createClient(API_URL, ANON_KEY, {
|
|
365
|
+
storage: {
|
|
366
|
+
async get() {
|
|
367
|
+
const raw = await AsyncStorage.getItem('session');
|
|
368
|
+
return raw ? JSON.parse(raw) : null;
|
|
369
|
+
},
|
|
370
|
+
async set(session) {
|
|
371
|
+
if (session) await AsyncStorage.setItem('session', JSON.stringify(session));
|
|
372
|
+
else await AsyncStorage.removeItem('session');
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Compatibility matrix
|
|
379
|
+
|
|
380
|
+
The matrix is **not maintained by hand**. It lives in
|
|
381
|
+
[`COMPATIBILITY.md`](./COMPATIBILITY.md) and is generated by
|
|
382
|
+
`apps/server/tests/sdk-contract.integration.test.ts`, which runs every listed
|
|
383
|
+
supabase-js call against a live Motherbase server and a live Postgres. Each row
|
|
384
|
+
carries the id of the test that produced it.
|
|
385
|
+
|
|
386
|
+
Rows marked `Not supported` assert the current failure behaviour, so the day a
|
|
387
|
+
feature lands its test fails and the document has to be regenerated. That is the
|
|
388
|
+
mechanism that keeps this page from over-claiming (or under-claiming) again. CI
|
|
389
|
+
regenerates the file and fails if the committed copy differs.
|
|
390
|
+
|
|
391
|
+
Regenerate it locally with:
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
npm run test -w apps/server -- sdk-contract
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
More examples live in [`docs/sdk.md`](../../docs/sdk.md). Project Studio also
|
|
398
|
+
exposes a project-specific Connect page with the exact API URL, WebSocket URL and
|
|
399
|
+
project ID to paste into `.env`.
|